RSS资讯

码农街博客

API7 企业版新特性|强势支持 SOAP 协议,开启全新集成时代

<p>您是否也因企业级应用在不同平台和语言之间的集成而感到困扰?API7 企业版最新版本已经能够支持和整合 SOAP 协议,为您提供了完美的解决方案。</p> <p>SOAP 协议通过提供一种基于 XML 的消息传递机制,实现了跨平台、跨语言的应用通信。它具有丰富的安全性、可靠性和可扩展性,适用于各种复杂的企业级集成场景。API7 企业版最新版本已能完美支持和整合 SOAP 协议,让企业能够轻松地将现有的 SOAP 服务与其他现代化的应用和系统进行对接,实现高效、稳定的应用集成。</p> <p>选择 API7 企业版,获得无缝全面的解决方案,无需再为复杂的企业集成场景而烦恼!</p> <h2>SOAP 协议介绍</h2> <p>SOAP 协议通过将消息封装在 XML 格式中进行传输,实现了跨平台和跨语言的通信。它基于 HTTP 或其他协议,允许应用程序在不同的操作系统和开发环境之间进行交互。</p> <p><img src="https://static.apiseven.com/uploads/2023/05/23/sk86Q85T_SOAP-API7.jpg" alt="API7 and SOAP" referrerpolicy="no-referrer"></p> <p>SOAP 协议具有以下特点:</p> <ol> <li>可靠性:SOAP 协议提供了一套可靠的消息传递机制,确保消息的完整性和可靠性。通过使用基于 XML 的消息格式和内置的错误处理机制,SOAP 协议可以处理网络中的传输错误,从而保证消息的正确传递。</li> <li>安全性:SOAP 协议支持多种安全机制,如加密和数字签名,以确保通信过程的安全性和数据的保密性。通过使用安全性扩展和标准的安全协议,如 HTTPS,SOAP 协议能够在企业级集成中处理敏感信息的传输和存储。</li> <li>可扩展性:SOAP 协议具有良好的可扩展性,允许定义自定义的消息和数据类型。通过使用 WSDL(Web Services Description Language)描述服务接口和消息结构,SOAP 协议可以适应不同的业务需求和应用场景。</li> </ol> <p>许多企业仍在使用旧有的 SOAP 服务,并希望将其整合到现代化的应用和系统中,例如将 SOAP 转 REST,可以方便用户以 RESTful 的方式访问传统的 Web 服务,降低 SOAP 客户端的开发成本。但直接改造 SOAP 服务成本过高,急需由 API 管理提供一种轻量而高效的解决方案。</p> <h2>API7 实现 SOAP 零成本转换</h2> <p>API7 企业版即将支持将普通 RESTful HTTP 请求转发给 soap-proxy 进程,从而实现 RESTful 请求到 SOAP 请求之间转换,无需对原有 SOAP 服务做任何改造。这使得企业能够轻松地将现有的 SOAP 服务与 API 网关进行集成,实现更高效、稳定的应用集成,让企业能够根据实际需求选择最合适的协议进行通信,并实现不同系统之间的互操作性。</p> <p>使用 API7 SOAP 插件及代理的优势:</p> <ul> <li>无需手动解析或导入 WSDL 文件</li> <li>无需定义转换模板</li> <li>无需编写任何转换或耦合代码</li> <li>WSDL URL 可以绑定到任何路由,可以在运行时更新,无需重启,配置动态生效</li> <li>无需解析和配置 WSDL 文件,自动识别服务 URL(上游地址)并用作 SOAP 上游</li> </ul> <p>传统的代理方式,要么提供转换模板,要么编写转换代码,都需要用户深度分析 WSDL 文件,存在不可忽视的开发成本。然而,API7 企业版提供了一种自动化的方式,自动分析 WSDL 文件,自动为每个操作提供转换逻辑,为用户消除开发成本。通过 API7 的自动转换功能,用户只需简单地配置 WSDL 的 URL,即可将现有的 SOAP 服务转换为 REST API。这个通用的程序不需要针对特定需求进行二次开发,可以适用于任何 Web 服务。由此一来,能为企业大大降低开发人员的工作量,并提高 API 开发的效率。</p> <p>API7 对 SOAP 的支持将为各个行业和应用领域带来许多好处。例如,银行、保险和支付机构等金融机构通常使用SOAP 协议进行安全的数据传输和交互。通过使用 API7,这些机构可以轻松地将现有的 SOAP 服务对接到网关中,从而实现中心化统一网关管理,降低调用方的使用门槛。同时,利用 API7 提供的安全特性,如身份验证、访问控制和传输层安全性,这些机构可以保护敏感的金融数据。</p> <h2>欢迎联系我们</h2> <p>由于历史原因和成本考虑,SOAP 服务并不总是适合完全重构为 RESTful 服务。因此,许多企业用户对 SOAP-to-REST 的需求很大。然而,由于 SOAP 和 REST 有着不同的架构和通信方式,将 SOAP 服务重构为 RESTful 服务的成本非常高。</p> <p>API7 提供了丰富的安全性、可靠性和可扩展性等特性,以及高效的 API 管理工具和分析功能,帮助企业用户实现高效、稳定的应用集成。欢迎与我们联系 <a href="https://api7.ai/contact">https://api7.ai/contact</a> 获取个性化的支持和解决方案。</p>

强强联手|星阑科技与支流科技达成战略合作

<p>近日,北京星阑科技有限公司(简称“星阑科技”)与深圳支流科技有限公司(简称“支流科技”)达成战略合作,双方将携手助力 API 基础设施发展,通过 API 生命周期管理+风险管理一体化解决方案,为 API 业务提供便捷性与安全性,赋能企业数字化转型。</p> <p><strong>【星阑科技 CTO 徐越寄语】</strong></p> <p>API 作为连接软件世界的媒介,广泛应用于各行业数字化业务场景。API迭代速度快、业务特性强,传统漏洞视角下的通用安全方案收效甚微,导致近年来数据泄露事件频发。本次合作星阑科技将携手支流科技共同探索API业务原生的技术服务体系,融合API生命周期管理与风险管理能力,实现安全赋能业务的愿景。</p> <p><strong>【支流科技 CTO 王院生寄语】</strong></p> <p>支流科技与星阑科技携手合作,完善了 API 全场景化解决方案矩阵,清晰化 API 全生命周期产品链条,提高 API 安全防护能力的同时,为用户打造完整高效的 API 管理方案。可及时发现并有效处理 API 安全风险,实时拦截恶意 API 请求,对企业敏感数据进行管控,保障企业用户的 API 数字资产,深度赋能企业用户的业务发展。</p> <p>星阑科技与支流科技双方将利用各自产品优势与品牌影响力,全力推进 API 基础设施与安全一体化能力建设。在业务应用 API、数据共享 API、云服务 API 等多种场景实现解决方案融合,避免业务与安全的重复建设,实现原生、高效的 API 业务管理目标,推进 API 经济与生态高速发展。</p> <p><img src="https://static.apiseven.com/uploads/2023/05/18/Y8pU7gqb_qgpCzt3nsP.jpg" alt="星阑科技与支流科技合作示意图" referrerpolicy="no-referrer"></p> <p>通过控制面层面的打通,API7 作为连接客户端和服务端的网关,能够向萤火 API 安全分析平台提供更多维的 API 服务数据,同时萤火基于大数据分析能力为 API 通信提供可观测性及安全审计,并将安全防护策略动态回传 API7 实现自动化安全防护。</p> <p>未来,星阑科技与支流科技将一如既往,砥砺前行,共同探索 API 经济的多元生态,迎接未来数字化经济的新征途。</p> <h3>关于星阑科技</h3> <p>星阑科技成立于 2018 年,是一家致力于 API 风险管理领域的创新企业。公司以安全攻防、数据智能技术为核心,为客户提供企业级 API 运行时风险管理解决方案。目前核心产品“萤火 API 安全管理平台”实现了 API 入侵风险、数据风险、业务风险、合规风险等全方面覆盖,构建 API 业务的安全基石。</p> <h3>关于支流科技</h3> <p>支流科技成立于 2019 年,是一家致力于支持 API 全生命周期管理的公司。2019 年支流科技将高性能、可扩展的 API 网关 APISIX 开源并捐赠给 Apache 软件基金会,服务了全球各行业的用户,如 zoom、vivo、Airwallex、爱奇艺等。在 Apache APISIX 的成功基础之上,支流科技提供 API 设计、API 开发、API 门户、API 货币化等更多领域的解决方案。</p> <h2>万物互联的纽带——API</h2> <p>随着全球信息技术高速发展,数字化转型深入推进,API 作为万物互联的接口和通道,承担着不同复杂系统环境、组织机构之间的数据交互、数据传输的重任,是企业数字化转型过程中的关键交互枢纽。</p> <ul> <li><strong>提供标准化的接口</strong>:API 提供了标准化的接口,可以使得不同系统或服务之间进行通信和交互更加方便、快捷、高效。这就意味着在一个系统或服务上所做的改变可能会影响到其他系统和服务。</li> <li><strong>促进数据共享和整合</strong>:API 可以帮助将不同系统和服务中的数据整合和共享,以实现更大规模的数据处理和分析。例如,各大社交媒体平台的API可以让第三方开发者访问和使用其用户数据,从而促进了社交媒体平台之间的数据共享和整合。</li> <li><strong>推动创新和业务发展</strong>:API 可以促进创新和业务发展,使得开发者和企业能够更快速地构建和推出新的产品和服务。通过API,不同的应用程序和服务之间可以更容易地合作以及对外输出,这也为企业和开发者带来了更广阔的商业机会和利益。</li> </ul> <p>一方面,企业需要一个高性能、安全的统一流量入口;另一方面,基于API网关的多协议支持能力,能够将企业内部采用不同协议标准的接口连接起来,并对外暴露 API 接口。导致 API 被开发者广泛使用,呈指数级增长。与此同时,API 安全问题也在日益凸显。</p> <h2>构建企业数字化转型安全屏障</h2> <p>据 Akamai 的一项统计表示,API 请求已占所有应用请求的83%,预计 2024 年 API 请求命中数将达到 42 万亿次。而与此同时,API 安全威胁比 API 调用增长更迅猛。Gartner 的研究发现,在 2022 年,超过 90% web 应用程序遭到的攻击来自 API。国际权威调查显示,API 攻击流量在一年中增长了&nbsp;681%,94% 的所有失窃数据涉及 API 暴露安全。因此,如何在实现企业各系统互联互通基础上,保障 API 安全,从而保护企业安全,并助力企业成功实现数字化转型,成为各大企业的迫切需求之一。</p> <p>从企业的角度来看,使用多种业务呈现方式是为了满足广大用户不同的使用需求,以期望得到更高效的业务能力。但随着业务量越大,业务渠道也越来越多,暴露的 API 也越来越多,所以对于企业安全建设而言,需要以前端结合后端的方式,以传统安全防护设备为基石,以新型安全技术为手段,解决新型业务场景下的安全风险。</p> <p>此外,API 安全管理体系的建立,离不开API全生命周期模型的应用,通过在各个位点植入安全能力,从而全面提升整体 API 安全水位,即围绕 API 的“设计、开发、运行、下线”等不同阶段建立 API 全生命周期安全技术能力,同时建立 API 安全生产管理制度与流程加以管控。&nbsp;技术架构方面,通过以传统基础服务设施和 PaaS 能力平台作为底层支撑,在服务共享层开放多个 API 原子能力和元数据提供点,部分 API 和基础能力再经过聚合网关进行承载,最终向外输出相应的开放能力。</p> <p>毋庸置疑,API 安全正在受到越来越多的关注,市场的广阔前景也得到了行业的充分认可。双方战略合作将围绕API 安全领域的市场共同开发、技术交流、信息资源共享等方面展开。支流科技在 API 全生命周期管理方面具有优秀的表现,可以帮助用户全面提升 API 的安全性和可靠性,降低企业面临的安全风险,为企业数字化转型提供更好的支持和保障。星阑科技与支流科技的战略合作,意味着双方将共同应对API安全面临的挑战,提高 API 安全解决方案的研发能力和市场竞争力,为客户提供更加全面、高效、优质的 API 安全服务。以数据安全建设为基础,API 安全建设为核心,结合双方优势资源在安全产品、安全服务等领域全面深化合作,共同推进 API 安全业务高质量发展,为我国建设网络强国和数字中国贡献力量。</p> <h2>云原生 API 安全治理智能化</h2> <p>当前在数字化转型浪潮下,企业纷纷步入云原生的进程。以容器、微服务、API 为代表的云原生技术,重塑了云端应用的设计、开发、部署和运行模式,实现了自动化、易管理、可观测的全新 DevOps 体系,开发者和运维人员能够最大限度地提高生产力,更敏捷、更高效地进行应用迭代。云原生作为云计算技术的下半场,已经大范围普及并深入应用到了企业核心系统,在促进企业实现技术架构和应用模式的变革的同时,也带来了新的安全风险和挑战,云原生安全成为企业关注的焦点,其中 API 作为数据交换的“管道”,以每年 60% 以上的增速走入大家的视线,当 API 重构软件供应链的同时,也带来一系列安全风险。</p> <p>通过使用云原生技术和人工智能等先进技术,对 API 进行智能化管理和保护,提高API的安全性和可靠性。其核心原理就是将 API 的安全治理与云原生技术和人工智能相结合。通过云原生技术可以快速构建、部署和管理 API,同时利用人工智能技术对 API 进行实时监控、自动检测和自我修复等操作,从而实现 API 的智能化安全治理。</p> <p>随着企业应用程序逐步向云上转移,API 的数量和种类也在不断增加,如何保障这些 API 的安全性和可靠性成为了一个重要问题。API 安全领域将会更加注重智能化和云原生技术的应用,以提升 API 安全管理和治理的效率和可靠度。云原生 API 安全治理智能化可以给用户带来多方面的好处。首先,它可以提高 API 的安全性和可靠性,从而降低企业面临的安全风险。其次,它可以实现 API 的智能化管理和自我修复,减少人工操作和管理成本。最后,它还可以促进企业数字化转型,提升应用程序的性能和效率。</p> <p>星阑科技和支流科技的合作,将带来更加智能化的 API 安全治理,这将大大提高 API 安全治理的效率和精度,降低 API 安全治理的成本和人力投入;将推动 API 安全治理的在全生命周期的标准化和规范化,为整个行业建立起统一的 API 安全治理体系和标准,促进 API 安全治理的规范化和标准化,提高整个行业的 API 安全水平。</p>

重磅!API7 Cloud 上架三大云市场,一键实现云原生转型

<blockquote> <p><a href="https://api7.ai/cloud">API7 Cloud</a> 是一款全新的云原生 API 管理平台,可以为企业提供一站式的 API 解决方案。近日,云原生 API 管理平台 API7 Cloud 标准版 正式上架三大云市场(阿里云云市场、腾讯云千帆云市场、华为云云商店)。无论您的产品是正在进行云原生转型,还是已经在云上运行多年,API7 Cloud 都可以帮助您更轻松、更高效地管理和部署 API!</p> </blockquote> <p>API7 Cloud 是基于 Apache 软件基金会顶级开源项目 Apache APISIX 构建的 SaaS 平台,用于大规模部署、控制、可视化和监控 API,可以用来管理部署在任意位置的 API。API7 Cloud 现已上架 阿里云、腾讯云、华为云 三大云市场平台,新增线上便捷购买通道,为企业提供更完整、更便捷、更高效的云计算解决方案,从而为用户创造更多的价值。</p> <p>以下是各大云市场的购买通道:</p> <p>阿里云云市场</p> <p>购买链接:<a href="https://market.aliyun.com/products/56368007/cmjj00062167.html">https://market.aliyun.com/products/56368007/cmjj00062167.html</a></p> <p>腾讯云千帆云市场</p> <p>购买链接:<a href="https://market.cloud.tencent.com/products/38125">https://market.cloud.tencent.com/products/38125</a></p> <p>华为云云商店</p> <p>购买链接:<a href="https://marketplace.huaweicloud.com/contents/d0b67aad-f761-4d19-863c-a7038c0e963c">https://marketplace.huaweicloud.com/contents/d0b67aad-f761-4d19-863c-a7038c0e963c</a></p> <h2>API7 Cloud — 多云与混合云场景下的 API 管理利器</h2> <p>API7 Cloud 的定位是帮助企业解决多云和混合云的场景下统一管理 API 的使用问题。它的目标用户是那些业务上了云,且需要一款 API 管理工具的用户;或者不仅仅是上了云,而且是使用了多云或者混合云的用户。</p> <p>API7 Cloud 基于 Apache APISIX,围绕着 Apache APISIX 所提供的功能进行了产品化,旨在让用户更简单、更放心地配置和使用 APISIX。市面上有很多类似的产品,例如:Kong Konnect、Tyk Cloud、Mulesoft Anypoint Platform 和 Amazon API Gateway。但 API7 Cloud 集成了 APISIX 的优势,这些是 API7 Cloud 独有的,例如:</p> <ul> <li>基金会品牌:无品牌纠纷,实力过硬</li> <li>高性能:APISIX 的 QPS 能达到23,000,平均延迟仅0.6毫秒</li> <li>社区活跃:APISIX 社区响应快,迭代更新速度快</li> <li>生态强大:支持近 100 个插件,生态包容</li> </ul> <p>API7 Cloud 以 Apache APISIX 为基础,又对它进行了更企业化的定制。</p> <ul> <li>支持开源 APISIX 的所有插件功能</li> <li>强化了开源 APISIX 的动态能力,比如支持了动态的服务发现功能</li> <li>开放 API 且提供 SDK,允许自动化的 API 管理,允许通过程序集成 API7 Cloud 实现自动化</li> </ul> <p>在 2023 年即将支持 APISIX Gateway API 规范,未来允许用户在开源 APISIX、企业版以及 API7 Cloud 之间相互切换。由此一来,为用户节约数据迁移的成本。</p> <p><img src="https://static.apiseven.com/uploads/2023/05/08/OvKWGYx3_API7-Cloud-Architecture.png" alt="API7 Cloud Architecture" referrerpolicy="no-referrer"></p> <h2>联系我们</h2> <p>如果您正在考虑云原生转型,或者正在寻找一款高效的 API 管理平台,API7 Cloud 是您的不二之选!API7 Cloud 可以帮助企业快速构建和管理 API,提高开发效率和 API 的可用性和可靠性。我们致力于为客户提供高质量、可靠的服务和支持,让您的 API 管理工作更加高效和便捷。</p> <p>想要了解更多关于 API7 Cloud 的信息,欢迎联系我们 <a href="https://apiseven.mikecrm.com/pvdVjd5">https://apiseven.mikecrm.com/pvdVjd5</a>, 我们期待与您合作,让 API 管理更高效!</p> <p><img src="https://static.apiseven.com/uploads/2023/05/09/xKnYlHq0_YuwrCB4Ob1.jpg" alt="Try API7 Cloud" referrerpolicy="no-referrer"></p>

云原生 API 网关 APISIX 和创造它的商业公司

<p>API7.ai(支流科技)(以下简称 API7)是一家提供 API 处理和分析的开源基础软件公司,提供 API 网关、Kubernetes Ingress Controller、Service Mesh 等微服务和实时流量处理的产品和解决方案。全球已经有数千家企业用户在使用 Apache APISIX 和 API7 企业版产品处理核心的业务流量,包括金融、互联网、制造、零售、运营商等多个行业,包括爱奇艺、vivo、腾讯云、bilibili、奈雪的茶、NASA、Airwallex、European Factory Platform 等知名企业和机构。</p> <p>API7 于 2019 年开源了新一代云原生 API 网关 —— APISIX 并捐赠给 Apache 软件基金会(ASF)。此后,API7 一直积极投入支持 Apache APISIX 的开发、维护和社区运营,致力为全球企业管理并可视化 API 和微服务等关键业务流量,通过大数据和人工智能(AI)加速企业业务决策,驱动数字化转型。</p> <p>其主要产品和服务有 API7 企业版、API7 Cloud 等。</p> <h2>Apache APISIX</h2> <p>APISIX,是新一代的云原生 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。该项目于 2019 年 6 月 6 日开源,于 2019 年 10 月捐献给 ASF 后,同年 10 月 17 日进入 Apache 孵化器,并于 2020 年 7 月从 Apache 孵化器毕业,成为 Apache 软件基金会顶级项目。</p> <p>截至 2022 年年底,Apache APISIX 已有来自全球超过 500 位的代码贡献者,并且坚持每个月都会发布一个新版本,是全球开发者最活跃的 API 网关项目。</p> <h2>API7 企业版</h2> <p>API7 企业版是基于 Apache APISIX 由经验丰富的专家构建的企业 API 管理平台,这些专家曾与全球各领域顶尖公司合作,了解并满足企业的 API 管理需求,旨在为企业实现加速 API 设计、交付和发现。 它具有以下特性:</p> <ol> <li>多层网络:为您不同地区的集群提供数据保护和数据合规性</li> <li>API 生命周期管理:提供 API 网关、开发者门户、文档、模拟测试等强大功能</li> <li>多集群管理:使用一个仪表盘管理不同区域的集群</li> <li>身份验证:内置 RBAC,支持与 Keycloak/OAuth/Okta/2FA 等身份验证系统接口对接</li> <li>金丝雀发布:支持通过请求头或查询字符串进行更精细的金丝雀发布和蓝/绿部署</li> <li>可观测性:集成 Datadog、Prometheus、Grafana 的 Metrics、Tracing 和 Logging</li> <li>灵活扩展:使用 DP 和 CP 实现动态扩缩容,以处理系统峰值和突发流量;内置 100+ 插件,支持自定义插件</li> <li>开发者门户:快速生成 API 文档并发布您的 API,允许第三方开发人员使用您的 API</li> <li>友好的定价:按节点/API 调用数量计费,或联系我们定制定价</li> </ol> <h2>API7 Cloud</h2> <p>API7 Cloud 是基于 Apache APISIX 构建的 SaaS 平台,用于大规模部署、控制、可视化和监控 API,可以用来管理部署在任意位置的 API。 它具有以下特性:</p> <ol> <li>基于ASF 顶级项目 Apache APISIX:云原生、动态、高性能、社区活跃</li> <li>轻松整合:只需几个简单的步骤,即可使用 Cloud CLI 在不同的基础架构(Kubernetes、Docker、Bare Metal)中运行 Apache APISIX</li> <li>无供应商锁定:在本地、多云、混合云环境部署您的 API,无需担心迁移</li> <li>可视化:简单的 API 诊断,让您随时了解服务的运行状态</li> <li>按照用量付费:根据您的需求选择适当的计划,并根据 API 调用次数支付费用</li> </ol> <h2>备受认可的 API 管理领导者</h2> <p>2021 年 5 月,API7.ai(支流科技)进入中国信通院发布的首批开源供应商名录;2022 年 10 月,入选 2022 红鲱鱼全球 100 强;2022年底入选 Gartner Market Guide for API Gateways 报告;2023 年 3月 API7 企业产品通过 SOC 2 Type 1 审计。</p> <p>在未来,我们会不断拓宽视野,以用户满意度和体验感为基石,推出更多优秀的产品。敬请期待!</p> <p>即刻扫码申请体验 API7 产品,解锁更多高级功能!</p> <p><img src="https://static.apiseven.com/uploads/2023/04/13/pz5EOLWn_https___apiseven.mikecrm.com_pvdVjd5.png" alt="Try" referrerpolicy="no-referrer"></p>

2023 年十大 API 管理趋势

<h2>什么是 API?什么是 API 管理?</h2> <p>近期,AIGC(AI Generated Content,生成式人工智能)在各行业的应用日趋普及。AIGC 服务提供商通过 API 向外部提供其内容生成能力,使得用户能够便捷地获取 AIGC 应用相关内容。显然,API 成为 AIGC 应用的重要支柱。那么,究竟什么是 API 呢?</p> <p>API(Application Programming Interface,应用程序接口)是一系列预先定义的规则和约定,目的是用于不同软件应用之间的通信。API 使得一个软件应用程序(即客户端)能够请求另一个软件应用程序(即服务器)的功能与数据,从而促成不同系统间的互动和数据共享。借助 API,开发者能够利用其他应用程序的功能,从而更快速地构建和发布新应用。</p> <p>API 管理包括创建和发布 API、制定使用策略、控制访问权限、培养用户社区、搜集与分析使用统计数据以及报告性能等过程,通常包含 API 网关、开发者门户等组件。其中,API 网关作为关键组件,负责处理和转发请求,同时执行安全和性能策略,而开发者门户则是一个在线平台,为开发者提供 API 文档、密钥管理和其他相关资源。</p> <p>随着企业日益依赖 API 推进数字化转型,API 管理的重要性空前提升。在简要介绍了相关概念之后,接下来我们将探讨 API 管理的十大发展趋势。</p> <h2>一、API 安全性越来越重要</h2> <p>API 安全性是指保护应用程序和系统之间通过 API 交换数据和功能的过程。API 安全性的主要目标是确保数据和功能的正确性、可靠性和私密性,防止未经授权的访问和潜在的恶意攻击。API 安全性对于现代应用和企业服务至关重要,因为它们大量依赖于 API 进行数据交换和集成。以下是 API 安全性重要的几点原因:</p> <ol> <li> <p><strong>数据保护</strong>:API 通常用于传输敏感数据,如用户信息、交易细节和支付信息。确保 API 安全性可以防止数据泄露、篡改和丢失,保障用户和企业的信息安全。</p> </li> <li> <p><strong>系统完整性</strong>:通过确保 API 只能被合法用户和合规应用访问,可以维护系统完整性。这有助于防止恶意攻击者通过 API 破坏或控制系统。</p> </li> <li> <p><strong>信任和声誉</strong>:一个安全的 API 可以提高用户对企业服务的信任度,有助于建立良好的品牌声誉。相反,如果 API 安全性不足,可能导致企业声誉受损,用户流失。</p> </li> </ol> <p>为了保障 API 的安全性,通常我们可以利用 API 网关来管理安全功能,例如身份验证和访问控制,以保护 API 免受未经授权的访问和攻击。市场上已有许多 API 网关提供这些功能,其中之一便是 Apache APISIX。<a href="https://apisix.apache.org/">Apache APISIX</a> 是 Apache 软件基金会下的云原生 API 网关,兼具动态、实时、高性能等特点。它提供了一系列安全功能,以确保 API 的安全性。例如,Apache APISIX 支持通过 <code>key-auth</code>、<code>jwt-auth</code> 等插件进行身份验证,以及通过 <code>consumer-restriction</code> 等插件进行访问控制。这些功能帮助企业防止数据泄露,保护用户隐私及企业利益。</p> <h2>二、API 标准化日益重要</h2> <p>随着 API 的广泛应用,API 标准化变得越来越重要。以下是 API 标准化的几大好处:</p> <ol> <li> <p><strong>促进组织内部的协作和沟通</strong>:让不同团队和部门遵循统一的设计原则和规范,提高开发效率和质量。</p> </li> <li> <p><strong>增强 API 的安全性和稳定性</strong>:通过定义清晰的接口、数据结构和协议,防止错误或滥用的情况发生。</p> </li> <li> <p><strong>提升 API 的可扩展性和互操作性</strong>:通过遵循行业或社区认可的设计指南或最佳实践,使 API 能够适应不同的场景和需求。</p> </li> </ol> <p>在 API 标准化过程中,常见的 API 标准规范有 <a href="https://swagger.io/specification/">OpenAPI Specfication</a>。许多工具和平台都支持这种规范,以方便用户导入和管理 API。例如,<a href="https://github.com/apache/apisix-dashboard">Apache APISIX Dashboard</a> 就可以通过 OpenAPI 文档进行导入相关路由数据。</p> <p><img src="https://static.apiseven.com/uploads/2023/03/24/JoFz3ZOy_openapi.png" alt="import routes data" referrerpolicy="no-referrer"></p> <p>利用这些标准规范,团队可以轻松地在不同平台和工具之间共享和管理 API,进一步提高协作效率和 API 的可维护性。</p> <h2>三、云端 API 管理解决方案的普及</h2> <p>传统的 API 管理解决方案通常侧重于在本地部署和管理 API,这意味着企业需要购买、部署和维护硬件和软件资源,以支持 API 的开发、发布和监控。然而,随着业务的发展和云计算技术的普及,传统的 API 管理解决方案在可伸缩性、成本效益和跨平台集成方面面临一定的挑战。</p> <p>与此同时,云端 API 管理解决方案应运而生。这类解决方案充分利用了云计算的弹性、按需付费和跨平台特性,为企业提供了一种更为灵活、高效和可靠的 API 管理方式。云端 API 管理解决方案通常包括 API 网关、安全功能、监控和分析等组件,以支持企业在混合云和多云环境中实现 API 的统一管理。</p> <p>相比于传统的 API 管理解决方案,云端 API 管理解决方案拥有以下优势:</p> <ol> <li> <p><strong>高可用性</strong>:得益于云端 API 管理解决方案提供弹性的负载均衡与自动扩展功能、以及自动化的故障切换和灾难恢复能力,使得云端 API 管理解决方案具备更高的可用性。</p> </li> <li> <p><strong>降低成本</strong>:云端 API 管理解决方案可以降低 API 的开发、部署和维护成本,让企业专注于业务创新而无需担心基础设施的管理。</p> </li> <li> <p><strong>跨平台支持</strong>:云端 API 管理解决方案支持混合云和多云环境,实现跨平台的 API 集成和管理,让企业可以轻松地在不同云服务提供商之间迁移和扩展其 API。</p> </li> </ol> <p>值得一提的是,基于 Apache APISIX 的 <a href="https://api7.ai/cloud">API7 Cloud</a> 产品正是这样一种云端 API 管理解决方案。API7 Cloud 采用了现代化的云架构,可以帮助企业管理部署在混合云和多云上的 API,并高效、可靠地连接它们,相较于传统 API 解决方案,具有更多的优势和灵活性。</p> <h2>四、使用低代码 API 平台方便创建发布 API</h2> <p>低代码 API 平台是一种允许用户通过简单的图形界面和预构建的模块创建、发布和管理 API 的工具。这些平台旨在简化 API 开发过程,降低开发门槛,提高开发效率。</p> <p>一个具体的例子是,<a href="https://github.com/apache/apisix-dashboard">Apache APISIX Dashboard</a> 创建路由的时候,无需手动编写代码,可以使用拖拽方式进行插件编排组合不同的插件。</p> <p><img src="https://static.apiseven.com/uploads/2023/03/21/b35zInFq_plugin-config.png" alt="plugin-config" referrerpolicy="no-referrer"></p> <h2>五、API 市场的发展</h2> <p>随着 API 的普及,API 市场逐渐成为企业从各种供应商发现、评估和购买 API 的一种方式。API 市场可以帮助企业加速创新,降低开发成本。</p> <p>1、对于 API 供应商,API 市场可以提高他们的 API 的可见性和吸引力,增加他们的收入和客户群,以及利用市场的分析功能来优化他们的 API 策略和设计。</p> <p>2、对于 API 消费者,API 市场可以提供一个方便的一站式服务,让他们能够轻松地找到并使用各种高质量的 API 来满足他们的业务需求,并且节省了自己开发或维护这些 API 的时间和资源。</p> <p>3、对于整个 API 生态系统,API 市场可以促进多方之间的协作和创新,激发新的用例和价值。</p> <h2>六、更多 API 协议崛起</h2> <p>随着下一代 API 协议,如 <a href="https://api7.ai/blog/what-is-graphql">GraphQL</a> 和 <a href="https://api7.ai/blog/what-is-grpc-and-how-to-work-with-apisix">gRPC</a>,与当前主导但逐渐衰落的 <a href="https://api7.ai/blog/understanding-and-using-restful-apis">REST API</a> 展开竞争,越来越多的 API 协议得到了广泛应用。</p> <p>GraphQL 是由 Facebook 开发的一种数据查询和操作语言。它允许客户端根据其需求明确请求所需数据,并在一个请求中获取多个资源。这有助于减少数据传输量和提高性能。与 REST API 相比,GraphQL 的优势包括:</p> <ol> <li> <p><strong>灵活的数据请求</strong>:客户端可以指定所需的数据,避免了过度或不足的数据传输。</p> </li> <li> <p><strong>更高效的请求处理</strong>:通过单个请求获取多个资源,有助于减少网络往返次数。</p> </li> <li> <p><strong>实时数据更新</strong>:GraphQL 支持实时数据更新,可以及时响应客户端的数据变更需求。</p> </li> </ol> <p>gRPC 是由 Google 开发的一种高性能、开源的远程过程调用(RPC)框架。它允许客户端像调用本地方法一样调用服务端的方法。gRPC 使用 Protocol Buffers(Protobuf)作为接口定义语言和数据序列化格式,以实现高效的数据传输。与 REST API 相比,gRPC 的优势包括:</p> <ol> <li> <p><strong>gRPC 使用 Protobuf 进行数据序列化</strong>:相较于 JSON 格式,具有更高的性能和更小的数据体积。</p> </li> <li> <p><strong>gRPC 基于 HTTP/2 协议,支持双向流式传输,多路复用和内置的 TLS 安全性</strong>:比 REST API 基于 HTTP/1.1 协议的单向请求响应模式更快,更灵活,更安全。</p> </li> <li> <p><strong>gRPC 基于 Protobuf 定义 API</strong>:提供了原生的代码生成功能,可以自动生成客户端和服务器端的代码,支持多种编程语言,比 REST API 需要使用第三方工具如 Swagger 生成代码更方便,更一致。</p> </li> </ol> <p>为了适应这些新兴协议的需求,Apache APISIX 提供了一系列相关插件,以支持不同协议的处理。</p> <p>在 Apache APISIX 中,以下插件可以处理这些新兴 API 协议:</p> <ul> <li><a href="https://apisix.apache.org/docs/apisix/plugins/grpc-transcode/">grpc-transcode</a>:<code>grpc-transcode</code> 用于在 HTTP 和 gRPC 请求之间进行转换。</li> <li><a href="https://apisix.apache.org/docs/apisix/plugins/grpc-web/">grpc-web</a>:<code>grpc-web</code> 是一个代理插件,可以处理从 JavaScript 客户端到 gRPC Service 的 <a href="https://github.com/grpc/grpc-web">gRPC Web</a> 请求</li> <li><a href="https://apisix.apache.org/docs/apisix/plugins/degraphql/">degraphql</a>:<code>degraphql</code> 插件用于支持将 RESTful API 解码为 GraphQL。</li> </ul> <h2>七、人工智能与 API</h2> <p>API 管理平台正在利用机器学习和人工智能自动化任务,例如 API 发现、安全威胁检测和异常检测。这可以帮助企业减轻其 IT 团队的负担,并提高其 API 管理流程的效率和准确性。</p> <ol> <li> <p><strong>安全威胁检测</strong>:机器学习和人工智能可以帮助 API 管理平台实时监控并分析 API 流量,以便于及时发现并阻止任何恶意或异常的请求。</p> </li> <li> <p><strong>异常检测</strong>:机器学习和人工智能可以帮助 API 管理平台预测并诊断任何可能影响 API 性能或可用性的问题,以便于及时修复并优化。</p> </li> </ol> <h2>八、更加关注开发者体验</h2> <p>随着 API 变得越来越集中于业务运营,开发者体验变得越来越重要。API 管理平台正在增加更多的开发者友好功能,例如文档、测试工具和 SDK,以使开发者更容易使用 API。</p> <ol> <li> <p><strong>文档</strong>:文档是开发者了解和学习 API 的主要途径,因此文档应该清晰、完整、准确、及时地描述 API 的功能、参数、示例和错误码等信息。文档还应该提供交互式的控制台或沙盒,让开发者能够快速地测试和调试 API。</p> </li> <li> <p><strong>测试工具</strong>:测试工具是开发者验证和优化 API 的重要手段,因此测试工具应该方便、可靠、灵活地支持各种测试场景和需求。测试工具还应该提供实时的反馈和报告,让开发者能够及时地发现并解决问题。</p> </li> <li> <p><strong>SDK</strong>:SDK 是开发者集成和使用 API 的便捷方式,因此 SDK 应该覆盖各种主流的编程语言和平台,并且保持与 API 的同步更新。SDK 还应该遵循最佳实践和规范,让开发者能够轻松地理解和调用。</p> </li> </ol> <h2>九、API 分析的兴起</h2> <p>API 分析(API Analytics)是一种用于收集、分析和解释 API 使用情况数据的技术。随着 API 在软件和互联网行业的普及,API 分析应运而生,成为一种关键的管理和优化手段。以下是 API 分析兴起的几点原因:</p> <ol> <li> <p>随着云计算、大数据、物联网等技术的发展,API 已成为企业和开发者之间交换数据和功能的重要工具。这导致了对 API 分析的需求不断增长,以便更好地了解和优化 API 的性能。</p> </li> <li> <p>现代软件开发越来越多地采用微服务架构,将复杂的应用程序分解为多个独立的、可扩展的服务。这些服务通过 API 相互通信,因此对 API 分析的需求在这种架构下更加明显。</p> </li> <li> <p>API 分析可以帮助检测潜在的安全漏洞和违反合规性的行为,从而降低风险。</p> </li> </ol> <h2>十、更多的 API 通过无服务器(Serverless)架构提供服务</h2> <p>无服务器架构是一种云计算模式,它允许开发者在不管理服务器的情况下,部署和运行应用程序。</p> <p>要通过无服务器架构提供 API 服务,你只需要以下几个步骤:</p> <ol> <li> <p>选择一个无服务器平台,编写你的 API 逻辑代码,使用无服务器平台提供的编程语言和框架。</p> </li> <li> <p>在平台上配置你的 API 触发器,如 HTTP 请求,定时器,事件等。</p> </li> <li> <p>使用平台提供的相关工具部署你的 API 代码到无服务器平台,并测试其功能和性能。</p> </li> </ol> <p>使用无服务器架构拥有以下优势:</p> <ol> <li> <p>无服务器架构可以让 API 开发者专注于业务逻辑,而不用担心基础设施、部署、缩放等问题。</p> </li> <li> <p>无服务器架构可以根据 API 请求量自动调整资源,避免资源浪费或不足。</p> </li> <li> <p>无服务器架构可以提高 API 的响应速度和可靠性,因为它可以利用分布式的边缘计算节点来处理请求</p> </li> </ol> <p>Apache APISIX 在这方面也有所支持,包括 <a href="https://apisix.apache.org/docs/apisix/plugins/serverless/">serverless</a>,<a href="https://apisix.apache.org/docs/apisix/plugins/openfunction/">openfunction</a> 等插件。</p> <h2>总结</h2> <p>API 管理作为数字化转型战略的重要组成部分,正面临着诸多挑战和发展机遇。通过关注 API 管理的这十大趋势,企业可以更好地应对未来的挑战,抓住发展机遇,并实现业务的持续增长和创新。</p>

etcd vs PostgreSQL:探究两种不同数据存储方案的优缺点

<h2>历史背景</h2> <p>PostgreSQL 的实现始于 1986 年,由伯克利大学的 Michael Stonebraker 教授领导。经过几十年的发展,PostgreSQL 堪称目前最先进的开源关系型数据库。它有自由宽松的许可证,任何人都可以免费使用、修改和分发 PostgreSQL,不管是私用、商用还是学术研究目的。</p> <p>PostgreSQL 全方位支持 OLTP 和 OLAP,具有强大的 SQL 查询能力和大量扩展,能满足几乎所有商业需求,所以近年来越来越被受到重视。事实上,PostgreSQL 强大的扩展性和高性能使得它能模拟任何其他不同类型数据库的功能。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/23/3eL70Khj_220574582-f40965b5-9275-45ca-8d94-16f54cd8dc64.png" alt="postgres architecture" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikibooks.org/wiki/PostgreSQL/Architecture</em></p> <p>而 etcd 又是如何诞生的呢?它解决了什么问题?</p> <p>2013 年,有一个叫 CoreOS 的创业团队,他们构建了一个产品:Container Linux。它是一个开源、轻量级的操作系统,侧重自动化、快速部署应用服务,并要求应用程序都在容器中运行,同时提供集群化的管理方案,用户管理服务就像单机一样方便。</p> <p>他们希望在重启任意一节点的时候,用户的服务不会因此而宕机,导致无法提供服务,因此需要运行多个副本。但是多个副本之间如何协调,如何避免变更的时候所有副本不可用呢?</p> <p>为了解决这个问题,CoreOS 团队需要一个协调服务来存储服务配置信息、提供分布式锁等能力。怎么办呢?当然是分析业务场景、痛点、核心目标,然后是基于目标进行方案选型,评估是选择社区开源方案还是自己造轮子。这其实就是我们遇到棘手问题时的通用解决思路,CoreOS 团队同样如此。</p> <p>一个协调服务,理想状态下大概需要满足以下五个目标:</p> <ul> <li>高可用,数据多副本</li> <li>数据一致性,数据副本之间的版本校对</li> <li>低容量、仅存储关键元数据配置。协调服务保存的仅仅是服务、节点的配置信息(属于控制面配置),而不是与用户相关的数据,所以存储上不需要考虑数据分片,无需过度设计</li> <li>功能:增删改查,监听数据变化的机制。协调服务保存了服务的状态信息,若服务有变更或异常,相比控制端定时去轮询检查一个个服务状态,若能快速推送变更事件给控制端,则可提升服务可用性、以及减少协调服务不必要的性能开销</li> <li>运维复杂度</li> </ul> <p>从 <a href="https://zh.wikipedia.org/wiki/CAP%E5%AE%9A%E7%90%86">CAP 理论</a>上来说,etcd 属于 CP 系统。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/23/KCin3Lzq_220592786-0d6d9605-4fbe-43d4-9f7e-798b9b072af9.png" alt="etcd architecture" referrerpolicy="no-referrer"></p> <p>作为 kubernetes 集群的中枢组件 kube-apiserver 正是采用 etcd 作为底层存储。</p> <p>一方面,k8s 集群中资源对象的创建都需要借助 etcd 来持久化;另一方面,正是 etcd 的数据 watch 机制,驱动着整个集群的 Informer 工作,从而达到源源不断的容器编排!因此,从技术角度说,Kubernetes 采用 etcd 的核心理由有:</p> <ul> <li>etcd 采用 go 语言编写,和 k8s 技术栈一致,资源占用率低,部署异常简单。</li> <li>etcd 的强一致性、watch、lease 等特性是 k8s 的核心依赖。</li> </ul> <p>总而言之,etcd 是针对配置管理和分发这个特定需求而设计出来的分布式 KV 数据库,它是云原生软件, 开箱即用和高性能使得它在这个需求上优于传统数据库。</p> <p>要比对 etcd 和 PostgreSQL 这两个不同类型的数据库,需要在同一个需求上去看才客观。</p> <p>所以本文只针对配置管理这个需求来阐述两者的差异。</p> <h2>数据模型</h2> <p>不同数据库对用户所呈现的数据模型有所不同,它决定了数据库的适用场景。</p> <h3>key-value vs SQL</h3> <p>key-value 是 nosql 里面很流行的模型,也是 etcd 所采纳的设计,相比 SQL,它有什么好处呢?</p> <p>我们先来看 SQL。</p> <p>关系数据库维护表中的数据,提供了一种高效、直观和灵活的方式来存储和访问结构化信息。</p> <p>表(也称为关系)由包含一个或多个数据类别的列和包含该类别定义的一组数据的行(也称为表记录)组成。应用程序通过指定查询来访问数据,这些查询使用诸如 project 之类的操作来标识属性、选择来标识元组以及连接来组合关系。数据库管理的关系模型是由 IBM 计算机科学家埃德加·科德在1970年开发的。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/23/LwtGXNsS_220575902-0e7840f3-056e-4ec0-91b7-e82b77c6fb20.png" alt="relational database" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikipedia.org/wiki/Associative_entity</em></p> <p>每个表里面的记录没有唯一标识符,因为表被设计为可容纳多个重复行。如果需要做到 KV 查询,需要为表里面用作 key 的字段加上唯一索引。PostgreSQL 的索引默认是 btree,跟 etcd 一样,可以做 key 的范围查询。</p> <blockquote> <p>结构化查询语言 (SQL) 是一种编程语言,用于在关系数据库中存储和处理信息。关系数据库以表格形式存储信息,行和列分别表示不同的数据属性和数据值之间的各种关系。您可以使用 SQL 语句从数据库中存储、更新、删除、搜索和检索信息。您还可以使用 SQL 来维护和优化数据库性能。</p> </blockquote> <p>PostgreSQL 对 SQL 做了很多扩展,使得它是图灵完备的语言,使用 SQL 可以做任何复杂的操作,使得数据处理逻辑完全在服务端进行。</p> <p>而 etcd 的定位是配置管理,配置数据一般是哈希表,所以将数据模型定位为 key-value,相当于只有全局一张大表,你可以对这张表进行增删查改。这张表只有两个字段,一个 key,一个 value,key 必须是唯一的,而且带有版本信息,而 value 的类型不做任何假设,所以客户端需要获取全量的 value 做进一步处理。</p> <p>总的来说,etcd 的 kv 是而对 SQL 的简化,对于配置管理这个特定需求,更加方便和直观。</p> <h3>MVCC(多版本并发控制)</h3> <p>对于配置管理,数据版本化是其中一个刚需:</p> <ul> <li>查询历史数据</li> <li>通过比较版本可以知道数据的新旧</li> <li>watch 数据需要以版本为根据,以便实现增量通知</li> </ul> <p>etcd 和 PostgreSQL 都有 <a href="https://zh.wikipedia.org/wiki/%E5%A4%9A%E7%89%88%E6%9C%AC%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6">MVCC</a>,但是它们的差异在哪里呢?</p> <p>etcd 维护了一个全局递增的 64 位版本计数器(无需担心计数器溢出,因为即便一秒钟产生百万次更新,也需要几十万年才能用完),每个 key-value 在创建和更新的时候都会赋予版本。删除 key-value 时会创建一个墓碑,版本重置为0,也就是说,每次变更都产生新的版本,而不是原地更新。同时,etcd 保留了一个 key-value 的所有版本,并且对用户可见。此外,etcd 实现 MVCC 最大的好处是读写分离,读取数据无需加锁,满足了 etcd 以读为主的定位。</p> <p>与 etcd 不同,PostgreSQL 的 MVCC 不是为了提供递增版本号,而是为了实现事务,也就是各类隔离级别,它对用户是透明的。MVCC 是一种乐观锁,它允许并发更新。表的每一行都有事务 ID 的记录,与 etcd 类似,xmin 对应了创建事务 ID,xmax 对应了更新事务 ID。</p> <ul> <li>每个事务只能读取在它之前已经 commit 的事务</li> <li>更新如果遇到版本冲突,会进行匹配重试,以决定是否更新</li> </ul> <p>示例请查看: https://devcenter.heroku.com/articles/postgresql-concurrency</p> <p>但是事务 ID 不能用于配置的版本控制,原因如下:</p> <ul> <li>同一个事务内涉及到的所有行都被赋予同一个事务 ID,也就是说,它不是行级别的</li> <li>只能读取最新版本的行,无法做历史查询</li> <li>事务 ID 会变,因为事务 ID 是32位计数器,容易溢出,在 vacuum 的时候会被重置</li> <li>无法根据事务 ID 实现 watch</li> </ul> <p>所以 PostgreSQL 要做配置数据的版本控制,需要用其他形式来替代,没有开箱即用的支持。</p> <h2>客户端接口</h2> <p>接口设计决定了客户端的使用成本和资源消耗,通过分析接口差异,可以帮助我们如何选型。</p> <p>etcd 提供了 kv/watch/lease API,实践证明它们特别满足配置管理的操作需求,那么在 PostgreSQL 上这些接口又如何实现呢?</p> <p>PostgreSQL 对这些 API 没有开箱即用的功能,需要通过封装来实现,这里使用笔者开发的 pg_watch_demo 项目来做分析:</p> <p>https://github.com/kingluo/pg_watch_demo</p> <h3>grpc/http vs tcp</h3> <p>PostgreSQL 是多进程架构,每个进程只能处理一个 tcp 连接,使用自定义协议通过 SQL 提供功能,采用一问一答的交互模型 (同一时间只能有一个查询执行中,类似 http1的同一时间只能处理一个请求,多个请求需要形成 pipeline),资源消耗大且比较低效,对于 QPS 大的场景需要前置连接池代理(例如 <a href="https://github.com/pgbouncer/pgbouncer">pgbouncer</a>)来提高性能。</p> <p>而 etcd 是 golang 的多协程架构,提供 grpc 和 restful 两种接口,使用方便,客户端容易集成; 资源消耗小,每条 grpc 连接可并发多个查询。</p> <h3>数据定义</h3> <h4>etcd</h4> <pre><code class="language-proto">message KeyValue { bytes key = 1; // 创建 key 的 revision int64 create_revision = 2; // 更新 key 的 revision int64 mod_revision = 3; // 版本递增计数器,每次更新递增,但是 delete 时会被清零用作墓碑 int64 version = 4; bytes value = 5; // key 使用的 lease 对象,用作 ttl,如果是0则没有 ttl int64 lease = 6; } </code></pre> <h4>PostgreSQL</h4> <p>PostgreSQL 需要使用一个 table 来模拟 etcd 的全局数据空间:</p> <pre><code class="language-sql">CREATE TABLE IF NOT EXISTS config ( key text, value text, -- 等价于 `create_revision` 和 `mod_revision` -- 这里使用大整数的递增序列类型来模拟 revision revision bigserial, -- 墓碑 tombstone boolean NOT NULL DEFAULT false, -- 组合索引,先搜索 key,然后是 revision primary key(key, revision) ); </code></pre> <h3>get</h3> <h4>etcd</h4> <p>etcd 的 get 参数比较丰富:</p> <ul> <li>范围查询,例如 <code>key</code> 为 <code>/abc</code>,而 <code>range_end</code> 为 <code>/abd</code>,那么就可以获取以 <code>/abc</code> 为前缀的所有 key-value</li> <li>历史查询,指定 <code>revision</code>,或者指定 mod_revision 范围</li> <li>排序、限定返回数目</li> </ul> <pre><code class="language-proto">message RangeRequest { ... bytes key = 1; // 范围查询 bytes range_end = 2; int64 limit = 3; // 历史查询 int64 revision = 4; // 排序 SortOrder sort_order = 5; SortTarget sort_target = 6; bool serializable = 7; bool keys_only = 8; bool count_only = 9; // 历史查询 int64 min_mod_revision = 10; int64 max_mod_revision = 11; int64 min_create_revision = 12; int64 max_create_revision = 13; } </code></pre> <h4>PostgreSQL</h4> <p>postgres 可以通过 SQL 完成 etcd 的 get 功能,甚至提供更多复杂的功能,因为 SQL 本身是语言,不是固定参数的接口可比拟的,这里简单展示只获取最新 revision 的 key-value。由于主键是组合索引,本身可以按范围搜索,所以速度会很快。</p> <pre><code class="language-sql">CREATE FUNCTION get1(kk text) RETURNS table(r bigint, k text, v text, c bigint) AS $$ SELECT revision, key, value, create_time FROM config where key = kk and tombstone = false ORDER BY key, revision desc limit 1 $$ LANGUAGE sql; </code></pre> <h3>put</h3> <h4>etcd</h4> <pre><code class="language-proto">message PutRequest { bytes key = 1; bytes value = 2; int64 lease = 3; // 是否返回修改前的 kv bool prev_kv = 4; bool ignore_value = 5; bool ignore_lease = 6; } </code></pre> <h4>PostgreSQL</h4> <p>类似 etcd,更改不是原地执行,而是插入一个新行,赋予新的 revision。</p> <pre><code class="language-sql">CREATE FUNCTION set(k text, v text) RETURNS bigint AS $$ insert into config(key, value) values(k, v) returning revision; $$ LANGUAGE SQL; </code></pre> <h3>delete</h3> <h4>etcd</h4> <pre><code class="language-proto">message DeleteRangeRequest { bytes key = 1; bytes range_end = 2; bool prev_kv = 3; } </code></pre> <h4>PostgreSQL</h4> <p>类似 etcd,删除不是原地修改,而是插入一个新行,将 tombstone 字段设置为 true 以表示是墓碑。</p> <pre><code class="language-sql">CREATE FUNCTION del(k text) RETURNS bigint AS $$ insert into config(key, tombstone) values(k, true) returning revision; $$ LANGUAGE SQL; </code></pre> <h3>watch</h3> <h4>etcd</h4> <pre><code class="language-proto">message WatchCreateRequest { bytes key = 1; // 指定 key 的范围 bytes range_end = 2; // watch 的起始 revision int64 start_revision = 3; ... } message WatchResponse { ResponseHeader header = 1; ... // 为了提高效率,可返回多个事件 repeated mvccpb.Event events = 11; } </code></pre> <h4>PostgreSQL</h4> <p>PostgreSQL 没有内置的 watch 功能,需要结合触发器和 channel 来实现。<code>pg_notify</code> 能将数据发送到侦听某个 channel 的所有应用程序。</p> <pre><code class="language-sql">-- 触发器,用于 put/delete 事件的分发 CREATE FUNCTION notify_config_change() RETURNS TRIGGER AS $$ DECLARE data json; channel text; is_channel_exist boolean; BEGIN IF (TG_OP = 'INSERT') THEN -- 将改动用 JSON 编码 data = row_to_json(NEW); -- 从 key 提取分发的 channel name channel = (select SUBSTRING(NEW.key, '/(.*)/')); -- 如果当前有应用在 watch,则通过 channel 发送事件 is_channel_exist = not pg_try_advisory_lock(9080); if is_channel_exist then PERFORM pg_notify(channel, data::text); else perform pg_advisory_unlock(9080); end if; END IF; RETURN NULL; -- result is ignored since this is an AFTER trigger END; $$ LANGUAGE plpgsql; CREATE TRIGGER notify_config_change AFTER INSERT ON config FOR EACH ROW EXECUTE FUNCTION notify_config_change(); </code></pre> <p>由于是封装出来的 watch,所以需要客户端应用也要实现配合逻辑,以 golang 为例:</p> <ol> <li> <p>发起 listen。 一旦 listen,所有 notify 数据会缓存起来(在 PostgreSQL 和 golang 的 channel 层面都可能存在缓存)</p> </li> <li> <p>get_all(key_prefix, revision)。 读取从指定 revision 开始的所有存量数据,每一个 key 只会返回最新 revision 数据,已删除的数据自动去除。也可以不指定 revision(这是最常见的情形),它会读取 key_prefix 前缀的所有 key 的最新数据。</p> </li> <li> <p>watch 新数据,包括在第一步和第二步之间可能缓存起来的 notification,使得不错过这个时间窗口可能产生的新数据。对于已经在第二步读取过的 revision,在这步忽略掉。</p> </li> </ol> <pre><code class="language-go">func watch(l *pq.Listener) { for { select { case n := &lt;-l.Notify: if n == nil { log.Println("listener reconnected") log.Printf("get all routes from rev %d including tombstones...\n", latestRev) // 重连的时候根据断开前的 revision 断点续传 str := fmt.Sprintf(`select * from get_all_from_rev_with_stale('/routes/', %d)`, latestRev) rows, err := db.Query(str) ... continue } ... // 应用要维护一个状态,里面记录已经接受到的最新的 revision updateRoute(cfg) case &lt;-time.After(15 * time.Second): log.Println("Received no events for 15 seconds, checking connection") go func() { // 长时间没收到事件,则检查一下连接是否健康 if err := l.Ping(); err != nil { log.Println("listener ping error: ", err) } }() } } } log.Println("get all routes...") // 应用在初始化的时候应该全量获取当前所有的 key-value,然后通过 watch 来增量监控更新 rows, err := db.Query(`select * from get_all('/routes/')`) ... go watch(listener) </code></pre> <h3>transaction</h3> <h4>etcd</h4> <p>etcd 的事务是带有判断条件的多个操作的集合,事务做出的修改是原子提交的。</p> <pre><code class="language-proto">message TxnRequest { // 指定事务执行条件 repeated Compare compare = 1; // 条件满足要执行的多个操作 repeated RequestOp success = 2; // 条件不满足要执行的多个操作 repeated RequestOp failure = 3; } </code></pre> <h4>PostgreSQL</h4> <p>用 <code>DO</code> 命令可以执行任何命令,包括存储过程,它支持很多语言,例如自带的 plpgsql、python 等,用这些语言可以实现任何条件判断、循环等控制逻辑,比 etcd 更丰富。</p> <pre><code class="language-sql">DO LANGUAGE plpgsql $$ DECLARE n_plugins int; BEGIN SELECT COUNT(1) INTO n_plugins FROM get_all('/plugins/'); IF n_plugins = 0 THEN perform set('/routes/1', 'foobar'); perform set('/upstream/1', 'foobar'); ... ELSE ... END IF; END; $$; </code></pre> <h3>lease</h3> <h4>etcd</h4> <p>在 etcd 里面,可以创建 lease 对象,应用要定期去续约这个 lease 对象,使得它不过期。</p> <p>每个 key-value 可以绑定一个 lease 对象,当 lease 对象过期时,所有绑定它的 key-value 都会过期,相当于自动被删除了。</p> <pre><code class="language-proto">message LeaseGrantRequest { // lease 的存活时间 int64 TTL = 1; int64 ID = 2; } // lease 续约 message LeaseKeepAliveRequest { int64 ID = 1; } message PutRequest { bytes key = 1; bytes value = 2; // lease ID,用于实现 ttl int64 lease = 3; ... } </code></pre> <h4>PostgreSQL</h4> <ul> <li>在 PostgreSQL 里面可以通过外键来维护 lease,查询的时候,如果有关联的 lease 对象且过期,则视为墓碑。</li> <li>keepalive 请求更新 lease 表里面的 last_keepalive 时间戳。</li> </ul> <pre><code class="language-sql">CREATE TABLE IF NOT EXISTS config ( key text, value text, ... -- 通过外键来指定其绑定的 lease 对象 lease int64 references lease(id), ); CREATE TABLE IF NOT EXISTS lease ( id text, ttl int, last_keepalive timestamp; ); </code></pre> <h3>性能对比</h3> <p>PostgreSQL 需要通过封装来模拟 etcd 的各类 API,那么性能如何呢?</p> <p>这里有一个简单的测试结果:</p> <p>https://github.com/kingluo/pg_watch_demo#benchmark</p> <p><img src="https://static.apiseven.com/uploads/2023/02/22/Blwn598E_219956977-cb5ac69a-990e-4b3b-a6ca-74142b1c0a99.png" alt="etcd_vs_postgres" referrerpolicy="no-referrer"></p> <p>从结果可见,读写性能相差无几,而 PostgreSQL 甚至比 etcd 更快。</p> <p>另外,一个更新从发生到应用接收到事件的延时决定了更新的分发效率,PostgreSQL 和 etcd 也是相差无几,客户端和服务端都在同一个机器测试时,watch 延时小于1毫秒。</p> <p>但是 PostgreSQL 有如下缺陷值得说明:</p> <ul> <li>每个更新对应的 WAL 日志更大,磁盘 IO 比 etcd 多一倍</li> <li>CPU 耗费比 etcd 多</li> <li>基于 channel 的 notify 是事务级别的概念,对同一类资源进行更新,就会将更新发往同一个 channel,更新请求之间会抢夺互斥锁,导致请求串行化,也就是说,通过 channel 实现 watch,会影响 put 的并行化</li> </ul> <p>从这里也可以看到,为了实现同样的需求,我们对 PostgreSQL 需要更多的学习成本和优化成本。</p> <h2>存储</h2> <p>底层存储决定了性能,数据如何落地决定了数据库对内存、磁盘等资源的需求。</p> <h3>etcd</h3> <p>etcd 存储架构图:</p> <p><img src="https://static.apiseven.com/uploads/2023/02/22/OnSA38xh_220088804-b7eefdd7-ee2f-4ad1-9504-0e6617e776a6.png" alt="etcd storage" referrerpolicy="no-referrer"></p> <p>etcd 首先将更新写入日志(WAL,write-ahead log),并且刷到磁盘,以保证这笔更新不会丢失,一旦日志成功写入且经过大多数节点确认,就可以返回结果给客户端了。etcd 还会异步更新 treeIndex 和 boltdb。</p> <p>为了避免日志的无限增长,etcd 定期对存储做快照,快照之前的日志可以被删掉。</p> <p>etcd 对所有的 key 都在内存里面做索引(treeIndex),在其中记录每个 key 的版本信息,但是 value 只是保留对 boltdb 的指针(revision)。</p> <p>而 key 对应的 value 则是保存在磁盘里面,使用 boltdb 来维护。</p> <p>treeIndex 和 boltdb 都使用 btree 数据结构,众所周知,btree 对于查找和范围查找是高效的。</p> <p>treeIndex 的结构图:</p> <p><img src="https://static.apiseven.com/uploads/2023/02/22/rBeo45IH_220065824-26126e1d-6344-4f45-b99d-cf2b2f5bdacb.png" alt="etcd treeindex" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 4.0 BY-SA 版权协议): https://blog.csdn.net/H_L_S/article/details/112691481</em></p> <p>每个 key 被分为不同的 generation,每次删除结束一个 generation。</p> <p>value 的指针由两个整数构成,第一个整数 main 是 etcd 的事务 ID,而第二个整数 sub 表示在该事务里对这个 key 的更新 ID。</p> <p>boltdb 支持事务和快照,里面保存的是 revision 对应的值。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/22/mnHbLWxR_220035209-a1c60d0f-9eb9-4dce-b71d-56040298e7b1.png" alt="etcd boltdb" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 4.0 BY-SA 版权协议): https://blog.csdn.net/H_L_S/article/details/112691481</em></p> <p>写入数据示例:</p> <p>写入 <code>key="key1", revision=(12,1),value="keyvalue5"</code>,注意 treeIndex 和 boltdb 的红色部分变化:</p> <p><img src="https://static.apiseven.com/uploads/2023/02/22/ks3kVykN_220035322-b3d90e34-2c25-49d6-bf54-1a6da085dd5e.png" alt="etcd put" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 4.0 BY-SA 版权协议): https://blog.csdn.net/H_L_S/article/details/112691481</em></p> <p>删除 <code>key="key",revision=(13,1)</code>,treeIndex 产生新的空的 generation,在 boltdb 生成一个空值,key="13_1t", 这里的 <code>t</code> 表示 <code>tombstone</code>(墓碑)。 这也暗含了你无法读取墓碑,因为 treeIndex 里面的指针是 <code>(13,1)</code>,但 boltdb 里是 <code>13_1t</code>,无法匹配。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/22/onIOzBOj_220133172-aee23979-08b6-4e0f-aa09-a84828656e47.png" alt="etcd delete" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 4.0 BY-SA 版权协议): https://blog.csdn.net/H_L_S/article/details/112691481</em></p> <p>值得注意的是,etcd 对 boltdb 的读写都由一个单独的 goroutine 来调度,以减少对磁盘的随机读写,提高 IO 性能。</p> <h3>PostgreSQL</h3> <p>PostgreSQL 的存储架构图:</p> <p><img src="https://static.apiseven.com/uploads/2023/02/22/E5oF4odG_220045336-20decab2-17d2-4e35-8e8f-eff0542348ce.png" alt="postgres storage" referrerpolicy="no-referrer"></p> <p>与 etcd 类似,PostgreSQL 首先将更新追加到日志文件,日志刷盘成功才表示事务完成,同时将更新写入 shared_buffer 内存。</p> <p>shared_buffer 由所有表共享,它映射了表和索引。</p> <p>PostgreSQL 里面每张表都由多个表页(page)文件组成,每个表页 8 KB,一个表页包含多个行。</p> <p>除了表,索引(例如 btree 索引)也是由同样格式的表页文件组成,只不过这些表页文件比较特殊,互相关联形成树结构。</p> <p>PostgreSQL 有一个 checkpointer 进程,会定时将所有表和索引的被更改的表页文件刷进磁盘,每个 checkpoint 之前的日志文件可以被删掉回收,避免日志无限增长。</p> <p>表页结构:</p> <p><img src="https://static.apiseven.com/uploads/2023/02/23/CZ4aNJyn_220578965-23b2dfdd-286c-4593-86a5-498379ed8c99.png" alt="postgres page" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikibooks.org/wiki/PostgreSQL/Page_Layout</em></p> <p>索引的表页结构:</p> <p><img src="https://static.apiseven.com/uploads/2023/02/22/c3cbNzJY_220034795-e6ba47d2-d80c-4010-924a-98aa583a9df9.png" alt="postgres btree index" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikibooks.org/wiki/PostgreSQL/Index_Btree</em></p> <p>由于表页文件的分散,为了提高读性能,某些 SQL 语句的查询计划会考虑通过位图使得表页读取被顺序化,提高 IO 性能:</p> <pre><code>EXPLAIN SELECT * FROM tenk1 WHERE unique1 &lt; 100; QUERY PLAN -------------------------------------------------------------------​----------- Bitmap Heap Scan on tenk1 (cost=5.07..229.20 rows=101 width=244) Recheck Cond: (unique1 &lt; 100) -&gt; Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0) Index Cond: (unique1 &lt; 100) </code></pre> <h3>结论</h3> <p>PostgreSQL 和 etcd 的存储充分考虑了 IO 性能,而 etcd 更是将所有 key 的索引置于内存,它们也都考虑了磁盘顺序读写的批量操作优化。 所以你可以看到在上面的性能对比里面,PostgreSQL 和 etcd 的读写性能相差无几。</p> <p>但是相比 PostgreSQL,etcd 要求<a href="https://etcd.io/docs/v3.5/op-guide/hardware/">更大的内存容量和更快的磁盘</a>。</p> <h2>分布式</h2> <p>去中心化和数据一致性是 etcd 的特色,也是云原生的需求,而传统数据库如何满足这点?</p> <h3>etcd</h3> <p><img src="https://static.apiseven.com/uploads/2023/02/23/sDVeI2yi_220591252-9709f0eb-67a7-4dd3-a1ba-235b8af91602.png" alt="etcd raft nodes" referrerpolicy="no-referrer"></p> <p><a href="https://en.wikipedia.org/wiki/Raft_(algorithm)">Raft</a>是很流行的分布式协议,etcd 通过 raft 分发更新到多个节点,保证已提交数据被大多数节点确认过。</p> <p>raft 有严谨正确的角色定义,角色切换图如下:</p> <p><img src="https://static.apiseven.com/uploads/2023/02/23/vB0JCnHn_220582846-eb01a402-f660-4bf3-aff3-df7b7a14d88a.png" alt="etcd raft state machine" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 3.0 BY-SA 版权协议): https://raft.github.io</em></p> <p>默认情况下,所有读写都在 master 节点执行。</p> <p>写要一致性这个好理解,但这里值得说明的是一致性读,它确保了读来自已提交数据,并且是数据的最新版本,每次读到的版本都等于或大于上一次读的版本。</p> <p>一致性读的实现简单来说,就是 slave 节点从 master 节点获取最新版本,如果 slave 节点版本比 master 节点旧,则等待同步。</p> <p>可见,etcd 的读写负担都落在 master 节点上,分布式只是保证可用副本和数据一致性,但是没有负载均衡。</p> <h3>PostgreSQL</h3> <p>PostgreSQL 是传统数据库出身,没有自带 raft 等分布式协议的实现, 但是它具备了集群化所需的数据复制特性,并且结合第三方的 raft 组件, 可以实现和 etcd 一模一样的分布式系统。</p> <p>原生的 PostgreSQL 已经包含了以下基础特性:</p> <ul> <li>synchronous commit</li> <li>quorum replication</li> <li>failover trigger</li> <li>hot-standby</li> </ul> <p>在主节点上的事务提交可配置为需要多个节点都确认才算提交成功,并且确认节点数可配置为大多数节点(quorum)。</p> <p>相关资料: https://www.2ndquadrant.com/en/blog/evolution-fault-tolerance-postgresql-synchronous-commit/</p> <p>数据复制的角色可以被切换(failover trigger),也有 pg_rewind 等工具可截除未被大多数节点确认的数据,以便重新加入集群。</p> <p>hot-standby 则提供类似 etcd 那样的 serializable read,也就是能在从节点上读取已提交数据,但不保证是最新版本。</p> <p>相关配置示例:</p> <pre><code>-- set quorum sync replication in postgresql.conf -- assume you have 5 nodes, then at least 2 standbys must be committed -- then you could tolerate 2 nodes failures synchronous_commit on synchronous_standby_names ="ANY 2 (*)" -- if master fails, check flushed lsn of each standby -- promote a standby with max lsn to master select flushed_lsn from pg_stat_wal_receiver; </code></pre> <p>PostgreSQL 在数据面上已经全面支持集群化,只需要在控制面提供 raft 组件即可实现去中心化的集群。笔者曾为多个商业客户提供 pg_raft 组件,该组件作为 PostgreSQL 的 worker process 运行,基于 raft 协议为 PostgreSQL 提供选主等集群管理功能。</p> <h2>维护</h2> <p>etcd 是为特定需求而设计的数据库,所以本身不需要怎么维护,这也是它的卖点之一。</p> <p>另一方面,由于优秀的设计,PostgreSQL 相比其他关系数据库, 需要 DBA 维护的点比较少,而且类似 etcd,很多维护工作都是 PostgreSQL 内置和自动进行的。</p> <p>数据库有很多维护的例行任务,这里只关注两点,compaction 和快照备份。</p> <h3>compaction</h3> <p>数据多版本化会使得数据库变得臃肿,读写效率变低,很多旧版本的数据当没有读取需要的时候应该要删除,并且要将删除后的空洞部分合并,这就是 compaction。</p> <p>etcd 在 API 上提供了 compact 和 defrag 两个操作。</p> <p>compact 用于删除某个 revision 之前的所有旧版本数据,注意如果覆盖了某些 key 的最新版本,则会保留最新版本,例如 <code>compact 100</code>,但是前面有一个 <code>key=foo, revision=87</code> 的 key-value,那么会保留它,但是会删除 <code>key=foo, revision=65</code> 的 key-value,说白了,compact 不会丢每个 key 的当前数据版本。</p> <p>etcd 提供了 Auto Compaction 功能,例如可以指定每隔多少小时 compact 一次。</p> <p>compact 会在 boltdb 留下空洞,所以需要 defrag 来整合它们,但是 defrag 会涉及大量 IO 和阻塞读写,需要谨慎进行。</p> <p>另一方面,PostgreSQL 的 compact 也很简单,例如使用以下 SQL 删除 revision 为100之前的旧数据:</p> <pre><code class="language-sql">with alive as ( select r as revision from get_all('/routes/') ) delete from config where revision &lt; 100 and not exists ( select 1 from alive where alive.revision = config.revision limit 1 ); </code></pre> <p>如果需要定时执行,可使用 crontab 或者 <a href="https://github.com/citusdata/pg_cron">pg_cron</a> 来实现。</p> <p>而数据库自身的 MVCC 清理,PostgreSQL 有自带的 <a href="https://www.postgresql.org/docs/current/routine-vacuuming.html">vacuum</a> 命令(vacuum full 对应 etcd 的 defrag),也有自动化的 autovacuum。</p> <h3>快照备份</h3> <p>快照备份可用于应急恢复,是数据库维护的刚需任务。</p> <p>etcd 提供了 API 创建和恢复快照,例如:</p> <pre><code class="language-bash">$ etcdctl snapshot save backup.db $ etcdctl --write-out=table snapshot status backup.db +----------+----------+------------+------------+ | HASH | REVISION | TOTAL KEYS | TOTAL SIZE | +----------+----------+------------+------------+ | fe01cf57 | 10 | 7 | 2.1 MB | +----------+----------+------------+------------+ $ etcdctl snapshot restore backup.db </code></pre> <p>PostgreSQL 也有很完善的备份工具:</p> <ul> <li><a href="https://www.postgresql.org/docs/current/app-pgbasebackup.html">pg_basebackup</a> 为新建 pg 从节点做数据准备</li> <li><a href="https://www.postgresql.org/docs/current/app-pgdump.html">pgdump</a> 在线克隆数据库实例,可选择备份哪些表</li> </ul> <p>事实上,基于 WAL 和逻辑复制,PostgreSQL 还支持更高级的备份机制,请参见如下链接:</p> <p>https://www.postgresql.org/docs/current/continuous-archiving.html</p> <p>https://www.postgresql.org/docs/current/logical-replication.html</p> <h2>结论</h2> <p>PostgreSQL 是通用型的传统 SQL 数据库,etcd 是专用的分布式 KV 数据库。</p> <p>相比 etcd 这类纯粹的数据存取系统,PostgreSQL 起码有如下额外好处:</p> <ol> <li>丰富的<a href="https://www.postgresql.org/docs/current/auth-methods.html">鉴权机制</a>,能实现完整的 RBAC 和细粒度的权限控制,支持多租户(多数据库实例),能过滤 IP,无需额外代理</li> <li>SQL 自带 schema,支持外键,无需提供额外的控制面逻辑去保证数据的完备性</li> <li>支持 JSON 类型的字段,支持基于 JSON 的索引和各类 JSON 操作,例如对路由配置进行索引以便做路由匹配</li> <li>支持数据加密,也支持通过 fdw 访问 hashicorp vault 获取 secret</li> <li>逻辑复制可实现多套独立集群之间的数据同步</li> <li>有存储过程加持,可实现额外的功能,例如实现上游的慢启动</li> </ol> <p>从功能上看,PostgreSQL 是 etcd 的超集,所以 PostgreSQL 可以通过其自带的丰富的基础功能和第三方组件来重现 etcd 的功能,也可以云化。</p> <p>用 PostgreSQL 来实现 etcd 的功能,相当于将航母改造为巡航舰,技术上完全没问题,但如果没有超出 etcd 能力范围的需求,那么这种做法的性价比很低,因为开发成本和维护成本是不可忽视的事实。</p> <p>etcd 最大的好处是开箱即用,满足了云原生时代对配置分发的需求,etcd 还可以用作应用选主、分布式锁、任务调度等功能的核心组件。</p>

WebAssembly 助力云原生:APISIX 如何借助 Wasm 插件实现扩展功能?

<h2>什么是 Wasm</h2> <p><a href="http://webassembly.org/">Wasm</a> 是 WebAssembly 的缩写。WebAssembly/Wasm 是一个基于堆栈的虚拟机设计的指令格式。 在 Wasm 未出现之前,浏览器中只能支持运行 Javascript 语言。当 Wasm 出现之后,使得高级语言例如 C/C++/Golang 能够在浏览器中运行。当前,主流的浏览器包括 Chrome、Firefox、Safari 等浏览器都已完成对 Wasm 的支持。并且得益于 WASI 项目的推进,服务端也已经能够支持运行 Wasm 指令。 如今在网关侧,Apache APISIX 也已完成对 Wasm 的支持,开发者可以通过高级语言 C/C++/Go/Rust 并按照 <a href="https://github.com/proxy-wasm/spec">proxy-wasm</a> 规范来完成 Wasm 插件的开发。</p> <p><img src="https://static.apiseven.com/uploads/2023/03/10/6ZI9czxe_whiteboard_exported_image.jpeg" alt="wasm" referrerpolicy="no-referrer"></p> <h2>为什么 APISIX 要支持 Wasm 插件</h2> <p>相比较原生的 Lua 插件,Wasm 插件存在如下优势:</p> <ul> <li> <p>可扩展性:APISIX 通过支持 Wasm,我们可以结合 proxy-wasm 提供的 SDK,使用 C++/Golang/Rust 等语言进行插件开发。由于高级语言往往拥有更加丰富的生态,所以我们可以依托于这些生态来实现支持更多功能丰富的插件。</p> </li> <li> <p>安全性:由于 APISIX 和 Wasm 之前的调用依托于 proxy-wasm 提供的 ABI(二进制应用接口),这部分的访问调用更为安全。Wasm 插件只允许对请求进行特定的修改。另外,由于 Wasm 插件运行在特定的 VM 中,所以即使插件运行出现崩溃也不会影响 APISIX 主进程的运行。</p> </li> </ul> <h2>APISIX 如何支持 WASM</h2> <p>了解完 Wasm,现在我们将从自顶向下的角度来看 APISIX 是如何支持 Wasm 插件功能的。</p> <p><img src="https://static.apiseven.com/uploads/2023/03/10/5t9ZpK7P_apisx-wasm.jpeg" alt="apisix-wasm" referrerpolicy="no-referrer"></p> <h3>APISIX Wasm 插件</h3> <p>在 APISIX 中,我们可以使用高级语言 C/C++/Go/Rust 来按照 proxy-wasm 规范以及对应的 SDK 来编写插件。</p> <blockquote> <p>proxy-wasm 是 Envoy 推出的在 L4/L7 代理之间的 ABI 的规范与标准。 在该规范中定义了包含内存管理、四层代理、七层代理扩展等 ABI。 例如在七层代理中,proxy-wasm 规范定义了proxy_on_http_request_headers,proxy_on_http_request_body,proxy_on_http_request_trailers,proxy_on_http_response_headers 等 ABI,使得模块能够在各个阶段对请求内容进行获取与修改。</p> </blockquote> <p>例如,我们使用 Golang 结合 <a href="https://github.com/tetratelabs/proxy-wasm-go-sdk">proxy-wasm-go-sdk</a>, 编写如下插件:</p> <blockquote> <p>proxy-wasm-go-sdk 正是上述 proxy-wasm 规范的 SDK,它帮助开发者更好的使用 Golang 编写 proxy-wasm 插件。 不过需要注意的是,由于原生 Golang 在支持 WASI 时存在一些问题,因此该 SDK 基于 TinyGo 实现,更多内容可以<a href="https://github.com/tetratelabs/proxy-wasm-go-sdk/blob/main/doc/OVERVIEW.md#tinygo-vs-the-official-go-compiler">点击</a>进行查看。</p> </blockquote> <p>该插件的主要功能用于将 HTTP 修改请求的响应状态码与响应体,引用自 APISIX <a href="https://github.com/apache/apisix/blob/master/t/wasm/fault-injection/main.go">链接</a>,</p> <pre><code>... func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { data, err := proxywasm.GetPluginConfiguration() if err != nil { proxywasm.LogErrorf("error reading plugin configuration: %v", err) return types.OnPluginStartStatusFailed } var p fastjson.Parser v, err := p.ParseBytes(data) if err != nil { proxywasm.LogErrorf("error decoding plugin configuration: %v", err) return types.OnPluginStartStatusFailed } ctx.Body = v.GetStringBytes("body") ctx.HttpStatus = uint32(v.GetUint("http_status")) if v.Exists("percentage") { ctx.Percentage = v.GetInt("percentage") } else { ctx.Percentage = 100 } // schema check if ctx.HttpStatus &lt; 200 { proxywasm.LogError("bad http_status") return types.OnPluginStartStatusFailed } if ctx.Percentage &lt; 0 || ctx.Percentage &gt; 100 { proxywasm.LogError("bad percentage") return types.OnPluginStartStatusFailed } return types.OnPluginStartStatusOK } func (ctx *httpLifecycle) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { plugin := ctx.parent if !sampleHit(plugin.Percentage) { return types.ActionContinue } err := proxywasm.SendHttpResponse(plugin.HttpStatus, nil, plugin.Body, -1) if err != nil { proxywasm.LogErrorf("failed to send local response: %v", err) return types.ActionContinue } return types.ActionPause } ... </code></pre> <p>之后,我们通过 tiny-go 将上述的 Golang 代码编译生成 <code>.wasm</code> 文件</p> <pre><code>tinygo build -o wasm_fault_injection.go.wasm -scheduler=none -target=wasi ./main.go </code></pre> <p>完成编译之后,我们得到了 <code>fault_injection.go.wasm</code> 文件</p> <blockquote> <p>如果对 wasm 文件内容感兴趣的话,我们可以使用 <a href="https://github.com/bytecodealliance/wasm-tools">wasm-tool</a> 工具来查看该 wasm 文件的具体内容。 <code>wasm-tools dump hello.go.wasm</code></p> </blockquote> <p>将 <code>wasm_fault_injection.go.wasm</code> 配置到 APISIX 到 <code>config.yaml</code>,并将该插件命名为 wasm_fault_injection。</p> <pre><code>apisix: ... wasm: plugins: - name: wasm_fault_injection priority: 7997 file: wasm_fault_injection.go.wasm </code></pre> <p>之后,我们启动 APISIX ,并创建一条路由引用该 Wasm 插件:</p> <pre><code>curl http://127.0.0.1:9180/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{ "uri":"/*", "upstream":{ "type":"roundrobin", "timeout":{ "connect":1, "read":1, "send":1 }, "nodes":{ "httpbin.org:80":1 } }, "plugins":{ "wasm_fault_injection":{ "conf":"{\"http_status\":200, \"body\":\"Hello WebAssembly!\n\"}" } }, "name":"wasm_fault_injection" }' </code></pre> <p>进行访问测试,发现响应体已被修改为 "Hello WebAssembly",由此 Wasm 插件已经生效。</p> <pre><code>curl 127.0.0.1:9080/get -v * Trying 127.0.0.1:9080... * Connected to 127.0.0.1 (127.0.0.1) port 9080 (#0) &gt; GET /get HTTP/1.1 &gt; Host: 127.0.0.1:9080 &gt; User-Agent: curl/7.81.0 &gt; Accept: */* &gt; * Mark bundle as not supporting multiuse &lt; HTTP/1.1 200 OK &lt; Date: Thu, 09 Feb 2023 07:46:50 GMT &lt; Content-Type: text/plain; charset=utf-8 &lt; Transfer-Encoding: chunked &lt; Connection: keep-alive &lt; Server: APISIX/3.1.0 &lt; Hello WebAssembly! </code></pre> <h3>Wasm-nginx-module</h3> <p>了解完 Apache APISIX 如何使用 Wasm 插件,现在我们更进一步来了解 "为什么我们能在 Wasm 插件中获取到请求的内容并修改请求?" 。 由于 APISIX 选用 Openresty 作为底层框架,因此 Wasm 插件中想要能够获取到请求内容和修改请求内容,就需要和 openResty 或者 NGINX 提供的 API 进行交互。<code>wasm-nginx-module</code> 正是提供了这部分能力。</p> <blockquote> <p>wasm-nginx-module 是由 <a href="https://api7.ai/">API7</a> 研发的支持 Wasm 的 NGINX 模块。 该模块尝试在 NGINX 的基础上实现 proxy-wasm-abi,并且向上封装了 Lua API,使得我们能够在 Lua 层面完成 proxy-wasm-abi 的调用。 更多内容可参考 <a href="https://github.com/api7/wasm-nginx-module">wasm-nginx-module</a>。</p> </blockquote> <p>例如,当我们的 APISIX 运行到 "access" 阶段时,会调用 <code>wasm-nginx-module</code> 中提供的 Lua 方法 on_http_request_headers。</p> <pre><code>-- apisix/wasm.lua ... local ok, err = wasm.on_http_request_headers(plugin_ctx) if not ok then core.log.error(name, ": failed to run wasm plugin: ", err) return 503 end end ... </code></pre> <p>之后在该方法中,将调用 <code>wasm-nginx-module</code> 中 <code>ngx_http_wasm_on_http</code> 方法,</p> <pre><code>ngx_int_t ngx_http_wasm_on_http(ngx_http_wasm_plugin_ctx_t *hwp_ctx, ngx_http_request_t *r, ngx_http_wasm_phase_t type, const u_char *body, size_t size, int end_of_body) { ... ctx = ngx_http_wasm_get_module_ctx(r); if (type == HTTP_REQUEST_HEADERS) { cb_name = &amp;proxy_on_request_headers; } else if (type == HTTP_REQUEST_BODY) { cb_name = &amp;proxy_on_request_body; } else if (type == HTTP_RESPONSE_HEADERS) { cb_name = &amp;proxy_on_response_headers; } else { cb_name = &amp;proxy_on_response_body; } if (type == HTTP_REQUEST_HEADERS || type == HTTP_RESPONSE_HEADERS) { if (hwp_ctx-&gt;hw_plugin-&gt;abi_version == PROXY_WASM_ABI_VER_010) { rc = ngx_wasm_vm-&gt;call(hwp_ctx-&gt;hw_plugin-&gt;plugin, cb_name, true, NGX_WASM_PARAM_I32_I32, http_ctx-&gt;id, 0); } else { rc = ngx_wasm_vm-&gt;call(hwp_ctx-&gt;hw_plugin-&gt;plugin, cb_name, true, NGX_WASM_PARAM_I32_I32_I32, http_ctx-&gt;id, 0, 1); } } else { rc = ngx_wasm_vm-&gt;call(hwp_ctx-&gt;hw_plugin-&gt;plugin, cb_name, true, NGX_WASM_PARAM_I32_I32_I32, http_ctx-&gt;id, size, end_of_body); } ... } </code></pre> <p>在 <code>wasm-nginx-module</code> 中,我们将根据不同的阶段,设置 <code>cb_name</code>,例如:<code>HTTP_REQUEST_HEADERS</code> 对应 <code>proxy_on_request_headers</code>,之后将在 <code>ngx_wasm_vm-&gt;call</code> 中调用 vm 中的方法也就是我们在上文中提到的 wasm 插件 <code>OnHttpRequestHeaders</code> 的方法。</p> <p>至此,整个 APISIX 调用 wasm 插件,运行 Golang 的调用链便梳理完成,调用链如下:</p> <p><img src="https://static.apiseven.com/uploads/2023/03/10/fzwAEDi4_call.png" alt="wasm-call" referrerpolicy="no-referrer"></p> <h3>Wasm VM</h3> <p>Wasm VM 用于真正执行 Wasm 代码的虚拟机,在 <code>wasm-nginx-module</code> 中实现了对两种虚拟机 "wasmtime" 和 "wasmedge" 两种虚拟机,在 APISIX 中默认选择使用 "wasmtime" 作为 Wasm 代码的运行虚拟机。</p> <blockquote> <p>Wasmtime <a href="https://github.com/bytecodealliance/wasmtime">Wasmtime</a> 是由 bytecodealliance 开源的 WebAssembly 和 WASI 的小型高效运行时。它能够在 Web 外部运行 WebAssembly 代码,即可以用作命令行使用,也可以作为 WebAssembly 运行引擎嵌入到其他程序作为库使用。 Wasmedge <a href="https://wasmedge.org/">Wasmedge</a> 是为边缘计算优化的轻量级、高性能、可扩展的 WebAssembly (Wasm) 虚拟机,可用于云原生、边缘和去中心化的应用。</p> </blockquote> <p>在 Wasm vm 中首先通过 <code>load</code> 方法将 <code>.wasm</code> 文件加载到内存,之后我们便可以通过 VM 的 call 方法来调用这些方法。VM 底层依托于 WASI 的接口实现,使得 Wasm 代码不仅能够运行在浏览器端,同时也支持能够在服务端进行。</p> <h2>总结</h2> <p>通过本文我们了解到 Wasm 是什么以及 APISIX 如何支持 Wasm 插件。APISIX 通过支持 Wasm 插件,不但可以扩充对多语言的支持,例如通过 C++, Rust, Golang, AssemblyScript 等进行插件开发,而且由于 WebAssembly 正在从浏览器走向云原生拥有了更加丰富的生态与使用场景,因此 APISIX 也可以借助 Wasm 完成在 API 网关侧更多的扩展功能,解决更多使用场景。</p>

什么是 LuaJIT?为什么 Apache APISIX 选择了 LuaJIT?

<h2>什么是 LuaJIT</h2> <h3>定义</h3> <p>简单地说,LuaJIT 是 Lua 这种编程语言的实时编译(JIT,Just-In-Time Compilation)器的实现。 对于不太了解 LuaJIT 的读者,我们可以将 LuaJIT 拆成 Lua 和 JIT 两个部分来理解。</p> <h4>Lua</h4> <p>Lua 是一种优雅、易于学习的编程语言,具有自动内存管理、完整的词法作用域、闭包、迭代器、协程、正确的尾部调用以及使用关联数组进行非常实用的数据处理。本文不会涉及 Lua 的语法,有关内容欢迎阅读 <a href="https://api7.ai/learning-center/openresty/getting-started-with-lua">Getting Started With Lua</a>。</p> <p>Lua 的设计目标是能与 C 或其它常用的编程语言相互集成,这样就可以利用其它语言已经做好的方面;而它提供的特性又恰好是 C 这类语言不太擅长的,比如相对于硬件层的高层抽象,动态的结构,简易的测试等等。其袖珍的语言内核和只依赖于 ANSI C 标准的特点,使之在各平台上的可移植性变得非常高。因此 Lua 不仅是一个可以作为独立程序运行的脚本语言,也是一个可以嵌入其它应用的嵌入式语言。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/24/TjmXxHHf_%E4%B8%8B%E8%BD%BD%20%281%29.png" alt="Apache APISIX 就是一个在底层同时使用 Lua 和 C 的极佳例子。" referrerpolicy="no-referrer"></p> <p>但此时的 Lua 还有传统脚本语言常见的两个问题:效率低和代码暴露。而 LuaJIT 引入的 JIT 技术能够有效地解决了这两个问题。</p> <h4>JIT</h4> <p>JIT(Just-In-Time Compilation),实时编译,是动态编译的一种形式。在计算机科学中,动态编译并不是唯一的编译形式,比如现今仍然流行的 C 语言使用的就是另一种形式:静态编译。</p> <p>需要指出的是,我们也常常将 C 语言的这种与动态编译相反的编译方式称为提前编译(AOT,Ahead-of-Time Compilation),但二者并不是完全对等的。AOT 仅是描述在执行程序前,将某种“高级”语言编译为某种“低级”语言的行为。其编译的目标语言并不一定特定于程序宿主机上的机器码,而是任意定义的。比如将 Java 编译为 C,或者将 JavaScript 编译为 V8 等等这些行为也会被视为 AOT。由于所有静态编译在技术上都是提前执行的,所以在这种特定的上下文中使用时,我们可以将 AOT 视为与 JIT 相反的静态编译。</p> <p>抛开这些冗杂的名词,想到静态编译的产物,你可能会发现,Lua 语言面临的问题也可以通过静态编译来解决。但事实上,这就丢失了 Lua 作为脚本语言的优势:热更新的灵活性和良好的平台兼容性。所以目前除了有特殊需求的脚本语言外,大部分脚本语言都在使用 JIT 尝试提高语言性能,比如 Chromium 平台上使用 V8 的 JavaScript 和使用 YJIT 的 Ruby。</p> <p>JIT 尝试将 Lua 的动态解释和 C 的静态编译两者的优缺点相结合,在脚本语言的执行期间,通过不断地分析正在执行的代码片段,编译或重新编译这段代码,以得到执行效率的提升。此时,JIT 假设的目标是,由此得到的性能提升能够高于编译或重新编译这段代码的开销。理论上说,由于能够进行动态地重新编译,JIT 在此过程中,可以针对正在运行程序的特定平台架构进行优化、加速,在某些情况下,能产生比静态编译更快的执行速度。</p> <p>JIT 分为传统的 Method JIT 和 LuaJIT 正在使用的 Trace JIT 两种。Method JIT 是将每一个方法(Method)翻译为机器码;而如下图所示,更先进的Trace JIT 假定 “对只执行一两次的代码,解释执行比 JIT 编译执行要快”,以此为依据对传统 JIT 进行优化,具体表现为将频繁执行的代码片段(即热路径上的代码)认定为需要跟踪的代码,将这部分代码编译成机器码执行。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/24/5tZgTwe1_%E6%97%A0%E6%A0%87%E9%A2%98-2023-02-24-1113.png" alt="LuaJIT 的原理" referrerpolicy="no-referrer"></p> <h4>LuaJIT</h4> <p>而 LuaJIT(2.x 版本)在 Trace JIT 的基础上,集成了使用汇编编写的高速解释器和基于 <a href="https://en.wikipedia.org/wiki/Static_single-assignment_form">SSA</a> 并进行优化的代码生成器后端,大幅提高了 JIT 的表现,最终使得 LuaJIT 成为最快的动态语言实现之一。</p> <p>除此之外,相对于原生 Lua 中为了与 C 交互而需要编写 Lua 与 C 的繁复绑定,LuaJIT 还实现了 FFI(外部函数接口,Foreign Function Interface)。该技术允许了我们在不清楚参数个数和类型的情况下,从 Lua 代码中直接调用外部的 C 函数和使用 C 的数据结构。由此功能,我们也可以直接使用 FFI 实现所需的数据结构,而非 Lua 原生的 Table 类型,进一步在性能敏感的场景下,提升程序运行的速度。有关使用 FFI 提高性能的技巧并非本文讨论的范畴,更深入的内容可以参阅 <a href="https://api7.ai/learning-center/openresty/why-lua-resty-core-perform-better">Why Does lua-resty-core Perform Better?</a>。</p> <p>总而言之,LuaJIT 在 Lua 语法的基础上,实现了迄今为止脚本语言中最快的 Trace JIT 之一,并提供了 FFI 等功能,解决了 Lua 效率低和代码暴露的问题,让 Lua 真正成为了高灵活性、高性能和超低内存占用的脚本语言和嵌入式语言。</p> <h3>与其它语言、WASM 的对比</h3> <p>相对于 Lua 和 LuaJIT,我们可能对其它的一些语言更加熟悉,比如 JavaScript (Node.js),Python,Golang,Java 等。对比这些大众化的语言,我们可以看到更多 LuaJIT 的特性和优势,下面简单罗列一些 Lua/LuaJIT 与这些语言的对比:</p> <ul> <li>Lua 的语法设计是针对非软件工程师所设计的。所以像 R 语言一样,Lua 也拥有数组下标从 1 开始等适合普通人的设计。</li> <li>Lua 非常适合作为嵌入式语言。Lua 本身拥有一个轻量的 VM,而 LuaJIT 在添加各种功能和优化后,也仍然很轻量。所以相对 Node.js 和 Python 之类庞大的运行环境,LuaJIT 直接集成到 C 编写的程序中后也不会增大太多体积。因此,实际上 Lua 是所有嵌入式语言中使用量比较大且主流的选择。</li> <li>Lua 也很适合做“胶水”语言。类似 JavaScript(Node.js) 和 Python,Lua 也能很好地连接不同的库和代码。但稍有不同的是,Lua 与底层生态的耦合性更高,所以在不同的领域中,Lua 的生态可能并不通用。</li> </ul> <p>WASM(Web Assembly)是一种新兴的跨平台技术。这种起初设计为补充而非取代 JavaScript 的技术,因为能够将其它的语言编译成 WASM 字节码,同时还能作为安全沙箱运行代码,使得越来越多的程序也在考虑使用 WASM 作为嵌入或者胶水的平台。即便如此,Lua/LuaJIT 在对比新兴的 WASM 时,也仍然有不少优势:</p> <ul> <li>WASM 的性能是受限的,无法达到汇编的水准。普遍场景下的性能,WASM 肯定好过 Lua,但与 LuaJIT 有所差距。</li> <li>WASM 与宿主程序的数据传递效率比较低。而 LuaJIT 可以通过 FFI 进行高效率的数据传递。</li> </ul> <h2>为什么 Apache APISIX 选择 LuaJIT</h2> <p>尽管上文描述了 LuaJIT 自身的诸多优势,但对于大部分开发者而言,Lua 不是一门大众的语言,LuaJIT 更不是一个大众的选择。那为什么 Apache 基金会所属的云原生 API 网关 Apache APISIX 还是选择了 LuaJIT 呢?</p> <p>作为云原生的 API 网关,Apache APISIX 兼具动态、实时、高性能等特点,提供了负载均衡、动态上游、灰度发布(金丝雀发布)、服务熔断、身份认证、可观测性等丰富的流量管理功能。我们可以使用 Apache APISIX 来处理传统的南北向流量,也可以处理服务间的东西向流量,还可以用作 k8s 的 Ingress Controller。</p> <p>而这一切都建立在 Apache APISIX 所选择的 NGINX 和 LuaJIT 技术栈之上。</p> <h3>LuaJIT 与 NGINX 结合带来的优势</h3> <p>NGINX 是一个知名的高性能 HTTP 、TCP/UDP 代理和反向代理的 Web 服务器。</p> <p>但在使用中,我们会发现很恼人的是,每次修改 NGINX 的配置文件后,都需要使用 <code>nginx -s reload</code> 重新加载 NGINX 配置。</p> <p>不仅如此,频繁地使用该命令重新加载配置可能会造成连接的不稳定,增加业务丢失的可能性;而在某些情况下,NGINX 重载配置的机制也可能会造成旧进程的回收时间过长,影响正常的业务。对于该问题的深入讨论,可以阅读 <a href="https://apisix.apache.org/zh/blog/2022/11/23/why-is-not-reload-hot-loaded-in-nginx/">为什么 NGINX 的 reload 不是热加载?</a>,这里不进行深入展开。</p> <p>Apache APISIX 诞生的目的之一就是解决 NGINX 的动态配置问题,LuaJIT 的高灵活性、高性能和超低内存占用带来了这种可能性。 以最具普遍性的路由为例,Apache APISIX 通过在 NGINX 配置文件中只配置单个 location 作为主入口,而后续的路由分发则由 APISIX 的路由分发模块完成,以此实现了路由的动态配置。</p> <p>为了实现足够高的性能,Apache APISIX 使用 C 编写了基于前缀树的匹配路由算法,并在此基础上使用 LuaJIT 提供的 FFI 编写了适用于 Lua 的接口。而 Lua 的灵活性,也使得 Apache APISIX 的路由分发模块,可以轻易地支持通过特定的表达式等方法,对同一前缀的下级路由进行匹配。最终在替代 NGINX 原生路由分发功能的前提下,实现了兼具高性能、高灵活性的动态配置功能。有关这部分功能的详细实现,可以查看 <a href="https://github.com/api7/lua-resty-radixtree">lua-resty-radixtree</a> 和 <a href="https://github.com/apache/apisix/blob/master/apisix/http/route.lua">route.lua</a>。</p> <p>另外,不只是路由,从负载均衡、健康检查,到上游节点配置、服务端证书,以及扩展 APISIX 能力的插件本身,都能在 APISIX 不重启的情况下重新加载。</p> <p>同时,除了在使用 LuaJIT 进行插件等功能的开发,Apache APISIX 还支持了 Java、Go、Node、Python 以及 WASM 等多种方式开发插件,也让 Apache APISIX 的二次开发门槛大大降低,使 Apache APISIX 获得了丰富的插件生态和活跃的开源社区。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/24/ugg7fXMD_%E4%B8%8B%E8%BD%BD%20%282%29.png" alt="Apache APISIX 的插件原理和生态" referrerpolicy="no-referrer"></p> <h2>总结</h2> <p>LuaJIT 是 Lua 的实时编译器实现。</p> <p>Apache APISIX 作为一个动态、实时、高性能的开源 API 网关,基于 NGINX 与 LuaJIT 带来的高性能、高灵活等特性,提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。</p> <p>目前 Apache APISIX 已经来到了全新的 3.x 版本,并带来了更多的开源项目集成、云供应商集成,原生的 gRPC 支持,更多的插件开发方式选择,以及服务网格支持等功能。欢迎加入 Apache APISIX 社区,了解更多 LuaJIT 在云原生 API 网关中的应用。</p>

本科期间没有任何荣誉,考研复试时影响大吗?

一笑而过官方账号的回答<br><br><h2><b>不仅影响不大且还有机会复试逆袭!</b></h2><p data-pid="qLXusOnc">✨恭喜你刷到这篇!暗示你顺利进复试啦! 小过今天来分享一些最近复试课上整理到的干货——本科摆烂复试如何逆袭,快快码住和我一起逆袭。</p><p data-pid="BRKH5ZbQ"> 其实很多宝都在担心本科期间没有太多拿的出手的成果该怎么办?不怕!以下5点可以帮助我们快速提升技能,速成『亮点』 </p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-d9a4f9feb51f1084e2e11a2822e3e4d4_1440w.jpg?source=b1748391" data-caption="" data-size="normal" data-rawwidth="440" data-rawheight="440" data-original-token="v2-fc8e1d557a3925456d9f4adacd3e6760" data-default-watermark-src="https://picx.zhimg.com/v2-33761f98f4638b98a394ce068adadb73_720w.jpg?source=b1748391" class="origin_image zh-lightbox-thumb" data-original="https://picx.zhimg.com/v2-d9a4f9feb51f1084e2e11a2822e3e4d4_r.jpg?source=b1748391" referrerpolicy="no-referrer"></figure><p data-pid="-PQnicwZ">✅提前学习研究生期间的专业课</p><p data-pid="z_jPuTSm"><b>coursera、慕课或哈佛大学等名校的在线课程都是很好学习途径</b>,找到专业相关课程,只用选一门学就好!学不懂、学个皮毛也没事,但<b>如果能学一个稍微简单的,周期短,能拿个证书的就更好了</b>!</p><p data-pid="iQi7cuD0">这样的话当复试老师问出你为什么选这个专业时,除了可以表达你对它的兴趣和热忱,更可以用你现在提前学习的本专业相关课程来作为论据支撑,<b>那回答的时候就可以做到有话可说啦</b>!</p><p data-pid="vXmt3xKs">✅看大牛的文章。</p><p data-pid="IV4XalLG">这是考研复试专业课准备中极其重要的一项!</p><ul><li data-pid="n67ktXAv">第一,你可以在院校官网中查找专业师资队伍找到学<b>科带头人</b>。</li><li data-pid="cyyperZd">第二,你可以问学长学姐院校比较牛的老师。</li><li data-pid="s-2IyelO">第三,你可以直接在百度上搜,某某领域带头人。即使不是目标院校的,但只要在该领域有见地,也可以。总之,<b>看大牛的文章</b>。</li></ul><p data-pid="21m7zsro">✅看核心期刊的论文。</p><p data-pid="slT8pS_r">在知网最左边有一个<b>【文献来源】</b>,你可以直接选择。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-4acd4e3bbb8d83026c0ac4600c3224f4_1440w.jpg?source=b1748391" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="293" data-original-token="v2-41907494e485bdb92910bcef3e846c71" data-default-watermark-src="https://picx.zhimg.com/v2-aea6ac0965a8362d8b70279d5afae43b_720w.jpg?source=b1748391" class="origin_image zh-lightbox-thumb" data-original="https://picx.zhimg.com/v2-4acd4e3bbb8d83026c0ac4600c3224f4_r.jpg?source=b1748391" referrerpolicy="no-referrer"></figure><p data-pid="0Ld7Sru9">图源来自知网</p><p data-pid="kYXh4kDD">当然,如果文章多,你还可以利用<b>【被引】【下载】</b>继续排序,哪个高,看哪个。</p><p data-pid="Tf32I7_p">✅关注专业领域热点。</p><p data-pid="Lkdxh3Ay">如果你不知道专业领域热点有哪些,你就去看<b>大牛最近都在写啥</b>,<b>核心期刊</b>最近都在发啥。</p><p data-pid="1gciFuQN">如果你不知道核心期刊都有什么,那就去百度。还有很多<b>线上的学术会议</b>,有心的同学去搜一下会有很多线上的学术会议是公开会议账号的,点进去听一听~</p><p data-pid="fgylrGuv"> 比如,当我们提前学习了专业课知识,当老师问到相关问题时,我们可以回答说: </p><p data-pid="qSCDBWmd">✨“老实说我在考完研后还提前在哈佛大学在线网课中学习了本专业的某门课程,对于A B C(核心术语、理论或者直接把课程的内容简介背一遍!)有了初步的了解。 在学习过程中发现自己确实被这个专业深深吸引。但是对于D F(来两个难一点的知识点)这两个地方还不太明白,不过我相信如果我有幸能被贵校录取的话,之后一定有很多机会向教授或同学请教。”</p><p data-pid="rQzH1GHT"> 再比如,当老师我们平时喜欢干什么,兴趣爱好是什么时,别人都说读书、运动,但我们就可以回答说: </p><p data-pid="M0F2btuF">✨“老师我比较喜欢做志愿者,因为我喜欢教学、喜欢帮助他人。所以在考完研后,我参加了某个项目的国际志愿者活动,主要内容就是在线教小朋友中文,为期两周。 在教学的过程中,我不仅感受到了… 更获得了…(说自己的心得感悟)我觉得这份独特的经历是我给自己未来研究生学习生活的一份礼物。如果有幸能被贵校录取的话,我想以后能在学校自己成立这样的社团,邀请身边的同学跟我一起做国际志愿者…”一下子厉害住了有没有!!!</p><p data-pid="IQoZBw3Q">✅ 做在线的国际志愿者</p><p data-pid="K8JHUVPp">(超洋气!!!)</p><p data-pid="m5rKtCRN">这一点特别适合<b>教育学、英语等相关专业</b>的同学。近几年由口罩原因,很多国际志愿者的活动都是<b>在线进行</b>的。大家可以去搜索一些短期线上教中文的项目,去教一些东南亚地区小朋友中文。</p><p data-pid="n-PbsmTA">这种经历不仅对于你自己是一种独特的体验,更重要的是项目结束后会有<b>证书</b>给你。这样当我们在考研复试的时候,如果老师问你平时喜欢干什么,兴趣爱好是什么,当别人都说看书、锻炼时,你就可以说:<b>其实我比较喜欢做志愿者</b>(有没有被自己装到!)。</p><p data-pid="l8Af-tKl">坚持到这里的每一个考研人都是最棒的!但是,<b>“行百里者半九十”</b></p><p data-pid="tn26yflx"><b>再冲一把,小过陪你岸上,不见不散❤️!</b></p>

知乎热榜

无需二次开发,SOAP-to-REST 简化企业用户的业务迁移和整合

<h2>1. 什么是 Web Service</h2> <p>Web Service 由万维网联盟 (W3C) 定义为一种软件系统,旨在支持通过网络进行可互操作的计算机间交互。</p> <p>Web Service 完成特定任务或任务集,并且由名称为 Web Service 描述语言 (WSDL) 的标准 XML 表示法中的服务描述进行描述。服务描述提供了与服务交互必需的所有详细信息,包括消息格式(用于详细说明操作)、传输协议和位置。</p> <p>其他系统使用 SOAP 消息与 Web Service 进行交互,通常是通过将 HTTP 与 XML 序列化和其他 Web 相关标准一起使用。</p> <p>Web Service 的架构图(注意现实中 Service broker 是可选的):</p> <p><img src="https://static.apiseven.com/uploads/2023/03/02/33VH3pCZ_222063798-1fb4eb54-8e39-4a51-a2c4-579e86d76476.png" alt="Web Services architecture" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikipedia.org/wiki/Web_service</em></p> <p>WSDL 接口隐藏服务实现方式的详细信息,这样服务的使用便独立于实现服务的硬件或软件平台,以及编写服务所使用的编程语言。</p> <p>基于 Web Service 的应用程序是松耦合、面向组件和跨技术的实现。 Web Service 可以单独使用,也可以与其他 Web Service 一起用于执行复杂的聚集或业务事务。</p> <p>Web Service 是 <a href="https://en.wikipedia.org/wiki/Service-oriented_architecture">Service-oriented architecture</a> (SOA) 的实现单元,SOA 是用来替换单体系统的一种设计方法,也就是说,一个庞大的系统可以拆分为多个 Web Service,然后组合起来对外作为一个大的黑盒提供业务逻辑。流行的基于容器的微服务就是 Web Service 最新替代品,但是很多旧系统都已经基于 Web Service 来实现和运作,所以虽然技术日新月异,兼容这些系统也是一个刚性需求。</p> <h3>WSDL (Web Services Description Language)</h3> <p>WSDL 是用于描述 Web Service 的一种 XML 表示法。 WSDL 定义告诉客户如何编写 Web Service 请求,并且描述了由 Web Service 提供程序提供的接口。</p> <p>WSDL 定义划分为多个单独部分,分别指定 Web Service 的逻辑接口和物理详细信息。物理详细信息既包括诸如 HTTP 端口号等端点信息,还包括指定如何表示 SOAP 有效内容和使用哪种传输方法的绑定信息。</p> <p><img src="https://static.apiseven.com/uploads/2023/03/02/BmuJlexJ_222070518-d8ed70a6-f226-4f9b-8a81-6357027cc9f1.png" alt="Representation of concepts defined by WSDL 1.1 and WSDL 2.0 documents." referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikipedia.org/wiki/Web_Services_Description_Language</em></p> <ul> <li>一个 WSDL 文件可以包含多个 service</li> <li>一个 service 可以包含多个 port</li> <li>一个 port 定义了 URL 地址(每个 port 都可能不同),可以包含多个 operation</li> <li>每个 operation 包含 input type 和 output type</li> <li>type 定义了消息结构:消息由哪些字段组成,每个字段的类型(可嵌套),以及字段个数约束</li> </ul> <h3>1.1 什么是 SOAP</h3> <p>SOAP 是在 Web Service 交互中使用的 XML 消息格式。 SOAP 消息通常通过 HTTP 或 JMS 发送,但也可以使用其他传输协议。 WSDL 定义描述了特定 Web Service 中的 SOAP 使用。</p> <p>常用的 SOAP 有两个版本:SOAP 1.1 和 SOAP 1.2。</p> <p><img src="https://static.apiseven.com/uploads/2023/03/02/Urf4a4Jq_222080939-d4bd2517-39a8-423d-b3d7-41905b72073e.png" alt="SOAP structure" referrerpolicy="no-referrer"></p> <p><em>图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikipedia.org/wiki/SOAP</em></p> <p>SOAP 消息包含以下部分:</p> <ul> <li>Header 元信息,一般为空</li> <li>Body <ul> <li>WSDL 里面定义的消息类型</li> <li>对于响应类型,除了成功响应,还有错误消息,它也是结构化的</li> </ul> </li> </ul> <p>例子:</p> <pre><code class="language-xml">&lt;SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"&gt; &lt;SOAP-ENV:Header&gt;&lt;/SOAP-ENV:Header&gt; &lt;SOAP-ENV:Body&gt; &lt;ns2:getCountryResponse xmlns:ns2="http://spring.io/guides/gs-producing-web-service"&gt; &lt;ns2:country&gt; &lt;ns2:name&gt;Spain&lt;/ns2:name&gt; &lt;ns2:population&gt;46704314&lt;/ns2:population&gt; &lt;ns2:capital&gt;Madrid&lt;/ns2:capital&gt; &lt;ns2:currency&gt;EUR&lt;/ns2:currency&gt; &lt;/ns2:country&gt; &lt;/ns2:getCountryResponse&gt; &lt;/SOAP-ENV:Body&gt; &lt;/SOAP-ENV:Envelope&gt; </code></pre> <h3>1.2 什么是 REST</h3> <p>Web Service 其实是一种抽象概念,本身可以有任何实现,例如 REST 就是一种流行实现方式。</p> <p>REST,即 Representational State Transfer 的缩写,直译就是表现层状态转化。 REST 这个词,是 Roy Thomas Fielding 在他 2000 年的博士论文中提出的。当时候正是互联网蓬勃发展的时期,软件开发和网络之间的交互需要一个实用的定义。</p> <blockquote> <p>长期以来,软件研究主要关注软件设计的分类、设计方法的演化,很少客观地评估不同的设计选择对系统行为的影响。而相反地,网络研究主要关注系统之间通信行为的细节、如何改进特定通信机制的表现,常常忽视了一个事实,那就是改变应用程序的互动风格比改变互动协议,对整体表现有更大的影响。我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。</p> </blockquote> <p>访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。HTTP 协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转化”。而这种转化是建立在表现层之上的,所以就是“表现层状态转化”。</p> <p>REST 四个基本原则:</p> <ol> <li>使用 HTTP 动词:GET POST PUT DELETE;</li> <li>无状态连接,服务器端不应保存过多上下文状态,即每个请求都是独立的;</li> <li>为每个资源设置 URI;</li> <li>通过 <code>x-www-form-urlencoded</code> 或者 JSON 作为数据格式;</li> </ol> <p>将 SOAP 转换为 REST,可以方便用户用 RESTFul 的方式访问传统的 Web Service,降低 SOAP client 的开发成本,如果能动态适配任何 Web Service,零代码开发,那就更完美了。</p> <p>REST 最大的好处是没有 schema,开发方便,而且 JSON 的可读性更高,冗余度更低。</p> <h2>2. SOAP-to-REST 代理的传统实现</h2> <h3>2.1 手工模板转换</h3> <p>这种方式需要为 Web Service 的每个 operation 提供 request 和 response 的转换模板,这也是很多网关产品使用的方式。</p> <p>我们可以使用 APISIX 的 body transformer plugin 来做简单的 SOAP-to-REST 代理,实践一下这种方式。</p> <p>作为例子,我们对上述 WSDL 文件里面的 <code>CountriesPortService</code> 的 <code>getCountry</code> 操作,根据类型定义构造 XML 格式的请求模板。</p> <p>这里我们将 JSON 里面的 name 字段填写到 getCountryRequest 里面的 name 字段。</p> <pre><code class="language-bash">req_template=$(cat &lt;&lt;EOF | awk '{gsub(/"/,"\\\"");};1' | awk '{$1=$1};1' | tr -d '\r\n' &lt;?xml version="1.0"?&gt; &lt;soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"&gt; &lt;soap-env:Body&gt; &lt;ns0:getCountryRequest xmlns:ns0="http://spring.io/guides/gs-producing-web-service"&gt; &lt;ns0:name&gt;{{_escape_xml(name)}}&lt;/ns0:name&gt; &lt;/ns0:getCountryRequest&gt; &lt;/soap-env:Body&gt; &lt;/soap-env:Envelope&gt; EOF ) </code></pre> <p>对于响应,就要提供 XML-to-JSON 模板,稍微复杂(如果要考虑 SOAP 版本间 fault 的差异,那就更复杂了),因为需要判断是否成功响应:</p> <ul> <li>成功响应,直接将字段一一对应填入 JSON</li> <li>失败响应,也就是 fault,我们需要另外的 JSON 结构,并且判断一些可选字段是否存在</li> </ul> <pre><code class="language-bash">rsp_template=$(cat &lt;&lt;EOF | awk '{gsub(/"/,"\\\"");};1' | awk '{$1=$1};1' | tr -d '\r\n' {% if Envelope.Body.Fault == nil then %} { "currency":"{{Envelope.Body.getCountryResponse.country.currency}}", "population":{{Envelope.Body.getCountryResponse.country.population}}, "capital":"{{Envelope.Body.getCountryResponse.country.capital}}", "name":"{{Envelope.Body.getCountryResponse.country.name}}" } {% else %} { "message":{*_escape_json(Envelope.Body.Fault.faultstring[1])*}, "code":"{{Envelope.Body.Fault.faultcode}}" {% if Envelope.Body.Fault.faultactor ~= nil then %} , "actor":"{{Envelope.Body.Fault.faultactor}}" {% end %} } {% end %} EOF ) </code></pre> <p>配置 APISIX 路由并且做测试:</p> <pre><code class="language-bash">curl http://127.0.0.1:9180/apisix/admin/routes/1 \ -H 'X-API-KEY: xxx' -X PUT -d ' { "methods": ["POST"], "uri": "/ws/getCountry", "plugins": { "body-transformer": { "request": { "template": "'"$req_template"'" }, "response": { "template": "'"$rsp_template"'" } } }, "upstream": { "type": "roundrobin", "nodes": { "localhost:8080": 1 } } }' curl -s http://127.0.0.1:9080/ws/getCountry \ -H 'content-type: application/json' \ -X POST -d '{"name": "Spain"}' | jq { "currency": "EUR", "population": 46704314, "capital": "Madrid", "name": "Spain" } # Fault response { "message": "Your name is required.", "code": "SOAP-ENV:Server" } </code></pre> <p>可见,这种方式需要人工去读懂 WSDL 文件里面每一个操作的定义,并且也要搞清楚每个操作对应的 web service 地址。如果 WSDL 文件庞大,包含大量操作和复杂的嵌套类型定义,那么这种做法是很麻烦的,调试困难,容易出错。</p> <h3>2.2 Apache Camel</h3> <p>https://camel.apache.org/</p> <p>Camel 是一个著名的 Java 整合框架,用于实现对不同协议和业务逻辑相互转换的路由管道,SOAP-to-REST 只是它的其中一个用途。</p> <p>使用 Camel 需要下载并导入 WSDL 文件,生成 SOAP client 的 stub 代码,使用 Java 编写代码:</p> <ul> <li>定义 REST endpoint</li> <li>定义协议转换路由,例如 JSON 字段如何映射到 SOAP 字段</li> </ul> <p>我们以温度单位转换的 Web Service 为例:</p> <p>https://apps.learnwebservices.com/services/tempconverter?wsdl</p> <ol> <li>通过 maven 根据 WSDL 文件生成 SOAP client 的代码</li> </ol> <p><code>cxf-codegen-plugin</code> 会为我们生成 SOAP client endpoint,用于访问 Web Service。</p> <pre><code class="language-xml">&lt;build&gt; &lt;plugins&gt; &lt;plugin&gt; &lt;groupId&gt;org.apache.cxf&lt;/groupId&gt; &lt;artifactId&gt;cxf-codegen-plugin&lt;/artifactId&gt; &lt;executions&gt; &lt;execution&gt; &lt;id&gt;generate-sources&lt;/id&gt; &lt;phase&gt;generate-sources&lt;/phase&gt; &lt;configuration&gt; &lt;wsdlOptions&gt; &lt;wsdlOption&gt; &lt;wsdl&gt;src/main/resources/TempConverter.wsdl&lt;/wsdl&gt; &lt;/wsdlOption&gt; &lt;/wsdlOptions&gt; &lt;/configuration&gt; &lt;goals&gt; &lt;goal&gt;wsdl2java&lt;/goal&gt; &lt;/goals&gt; &lt;/execution&gt; &lt;/executions&gt; &lt;/plugin&gt; &lt;/plugins&gt; &lt;/build&gt; </code></pre> <ol start="2"> <li>编写 SOAP client bean</li> </ol> <p>注意这里我们记住 bean 的名字是 <code>cxfConvertTemp</code>,后面定义 Camel 路由用到。</p> <pre><code class="language-java">import com.learnwebservices.services.tempconverter.TempConverterEndpoint; @Configuration public class CxfBeans { @Value("${endpoint.wsdl}") private String SOAP_URL; @Bean(name = "cxfConvertTemp") public CxfEndpoint buildCxfEndpoint() { CxfEndpoint cxf = new CxfEndpoint(); cxf.setAddress(SOAP_URL); cxf.setServiceClass(TempConverterEndpoint.class); return cxf; } } </code></pre> <ol start="3"> <li>先编写下游 REST 的路由</li> </ol> <p>从这个路由我们可以看到它定义了 RESTFul 风格的 URL 及其参数定义,并且定义了每个 URL 的下一跳路由。例如<code>/convert/celsius/to/fahrenheit/{num}</code>,将 URL 里面最后一个部分作为参数(double 类型)提供给下一跳路由<code>direct:celsius-to-fahrenheit</code>。</p> <pre><code class="language-java">rest("/convert") .get("/celsius/to/fahrenheit/{num}") .consumes("text/plain").produces("text/plain") .description("Convert a temperature in Celsius to Fahrenheit") .param().name("num").type(RestParamType.path).description("Temperature in Celsius").dataType("int").endParam() .to("direct:celsius-to-fahrenheit"); </code></pre> <ol start="4"> <li>最后编写上游 SOAP 路由及上下游的转换</li> </ol> <pre><code class="language-java">from("direct:celsius-to-fahrenheit") .removeHeaders("CamelHttp*") .process(new Processor() { @Override public void process(Exchange exchange) throws Exception { // 初始化 SOAP 请求 // 将下游参数 num 填写到 body,body 就是一个简单的 double 类型 CelsiusToFahrenheitRequest c = new CelsiusToFahrenheitRequest(); c.setTemperatureInCelsius(Double.valueOf(exchange.getIn().getHeader("num").toString())); exchange.getIn().setBody(c); } }) // 指定 SOAP operation 和 namespace // 在 application.properties 文件定义 .setHeader(CxfConstants.OPERATION_NAME, constant("{{endpoint.operation.celsius.to.fahrenheit}}")) .setHeader(CxfConstants.OPERATION_NAMESPACE, constant("{{endpoint.namespace}}")) // 交给 WSDL 生成的 SOAP client bean 去发包 .to("cxf:bean:cxfConvertTemp") .process(new Processor() { @Override public void process(Exchange exchange) throws Exception { // 处理 SOAP 响应 // 将 body,也就是 double 类型的值填充到字符串里面去 // 将字符串返回给下游 MessageContentsList response = (MessageContentsList) exchange.getIn().getBody(); CelsiusToFahrenheitResponse r = (CelsiusToFahrenheitResponse) response.get(0); exchange.getIn().setBody("Temp in Farenheit: " + r.getTemperatureInFahrenheit()); } }) .to("mock:output"); </code></pre> <ol start="5"> <li>测试</li> </ol> <pre><code class="language-bash">curl localhost:9090/convert/celsius/to/fahrenheit/50 Temp in Farenheit: 122.0 </code></pre> <p>可见,通过 Camel 做 SOAP-to-REST,就要针对所有 operation 用 Java 代码定义路由和转换逻辑,需要开发成本。</p> <p>同理,如果 WSDL 包含很多 service 和 operation,那么走 Camel 这种方式来做代理,也是比较痛苦的。</p> <h3>2.3 结论</h3> <p>我们总结一下传统方式的弊端。</p> <table> <thead> <tr> <th></th> <th>模板</th> <th>Camel</th> </tr> </thead> <tbody> <tr> <td>WSDL</td> <td>人工解析</td> <td>通过 maven 去生成代码</td> </tr> <tr> <td>上游</td> <td>人工解析</td> <td>自动转换</td> </tr> <tr> <td>定义 body</td> <td>提供模板做判断和转换</td> <td>编写转换代码</td> </tr> <tr> <td>获取参数</td> <td>nginx 变量</td> <td>在代码里面自定义或者调用 SOAP client 接口获取</td> </tr> </tbody> </table> <p>这两种方式都有开发成本,并且对每一个新的 Web Service,都需要重复这个开发成本。</p> <p>开发成本与 Web Service 的复杂度成正比。</p> <h2>3. APISIX 的 SOAP-to-REST 代理</h2> <p>传统的代理方式,要不提供转换模板,要不编写转换代码,都需要用户深度分析 WSDL 文件,有不可忽视的开发成本。</p> <p>APISIX 提供了一种自动化的方式,自动分析 WSDL 文件,自动为每个操作提供转换逻辑,为用户消除开发成本。</p> <p><img src="https://static.apiseven.com/uploads/2023/03/03/Ay0SHzvx_222647394-ec6bcf13-b9ff-439c-b84f-064a3a7ff1df.png" alt="APISIX SOAP-to-REST proxy" referrerpolicy="no-referrer"></p> <h3>3.1 无代码自动转换</h3> <p>使用 APISIX SOAP 代理:</p> <ul> <li>无需手工解析或导入 WSDL 文件</li> <li>无需定义转换模板</li> <li>无需编写任何转换或耦合代码。</li> </ul> <p>用户只需要配置 WSDL 的 URL,APISIX 会自动做转换,它适用于任何 Web Service,是通用程序,无需再针对特定需求做二次开发。</p> <h3>3.2 动态配置</h3> <ul> <li>WSDL URL 可绑定在任何路由,和其他 APISIX 资源对象一样,可在运行时更新配置,配置更改是动态生效的,无需重启 APISIX。</li> <li>WSDL 文件里面包含的 service URL(可能有多个 URL),也就是上游地址,会被自动识别并且用作 SOAP 上游,无需用户去解析并配置。</li> </ul> <h3>3.3 实现机制</h3> <ul> <li>从 WSDL URL 获取 WSDL 文件内容,分析后自动生成 proxy 对象</li> <li>proxy 对象负责协议转换 <ul> <li>根据 JSON 输入生成合规的 SOAP XML 请求</li> <li>将 SOAP XML 响应转换为 JSON 响应</li> <li>访问 Web Service,自动处理 SOAP 协议细节,例如 Fault 类型的响应</li> <li>支持 SOAP1.1和 SOAP1.2,以及若干扩展特性,例如 WS-Addressing</li> </ul> </li> </ul> <h3>3.4 配置示例</h3> <p>SOAP 插件的配置参数说明:</p> <table> <thead> <tr> <th>参数</th> <th>必选?</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td><code>wsdl_url</code></td> <td>是</td> <td>WSDL URL,例如 <code>https://apps.learnwebservices.com/services/tempconverter?wsdl</code></td> </tr> <tr> <td><code>operation</code></td> <td>否</td> <td>操作名,可来自任何 nginx 变量,例如<code>$arg_operation</code>或者<code>$http_soap_operation</code></td> </tr> <tr> <td><code>service</code></td> <td>否</td> <td>服务名,如果一个 WSDL 文件包含多个服务,可通过这个参数来指定访问哪个服务</td> </tr> <tr> <td><code>ca_cert</code></td> <td>否</td> <td>校验服务端证书的 CA 证书内容</td> </tr> <tr> <td><code>client_cert</code></td> <td>否</td> <td>用于 MTLS 的 client 证书内容</td> </tr> <tr> <td><code>client_key</code></td> <td>否</td> <td>用于 MTLS 的 client 私钥内容</td> </tr> </tbody> </table> <p>测试:</p> <pre><code class="language-bash"># 配置 APISIX 路由,使用 SOAP 插件 # 注意这里一条路由能执行所有操作,用 URL 参数来指定操作名 # 这也体现了动态代理的好处,不需要再手工去分析 WSDL 里面每一个操作 curl http://127.0.0.1:9180/apisix/admin/routes/1 \ -H 'X-API-KEY: xxx' -X PUT -d ' { "methods": ["POST"], "uri": "/ws", "plugins": { "soap": { "wsdl_url": "http://localhost:8080/ws/countries.wsdl", "operation": "$arg_operation", "service": "&lt;use alternative service defined in wsdl if exist&gt;", "ca_cert": "&lt;ca cert file content&gt;", "client_cert":"&lt;client cert file content&gt;", "client_key":"&lt;client key file content&gt;" } } }' curl 'http://127.0.0.1:9080/ws?operation=getCountry' \ -X POST -d '{"name": "Spain"}' # 成功响应 HTTP/1.1 200 OK Date: Tue, 06 Dec 2022 08:07:48 GMT Content-Type: text/plain; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Server: APISIX/2.99.0 {"currency":"EUR","population":46704314,"capital":"Madrid","name":"Spain"} # 失败响应 HTTP/1.1 502 Bad Gateway Date: Tue, 03 Jan 2023 13:43:33 GMT Content-Type: text/plain; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Server: APISIX/2.99.0 {"message":"Your name is required.","actor":null,"code":"SOAP-ENV:Server","subcodes":null,"detail":null} </code></pre> <h2>4. 结论</h2> <p>Web Service 发展至今,有大量企业用户使用传统的 SOAP based Web Service 提供服务,这些服务由于历史原因和成本考虑,不适合做 RESTFul 的完全重构,所以 SOAP-to-REST 对不少企业用户有刚性需求。</p> <p>APISIX 提供的 SOAP-to-REST 插件,能实现零代码的代理功能,可动态配置,无需二次开发,有利于企业用户的零成本业务迁移和整合。</p>

即将推出 | 让 API 管理效率更进一步的 API7 DevPortal

<p>当今计算机世界,人们早已习惯使用 API 在软件之间完成信息的交换,不论是你在手机上查看天气信息、查看微信朋友圈的动态、亦或是和 ChatGPT 交互,其实都是通过 API 来完成的。从定义上来说,API 是一组通信的约定,它规定了你(或者软件)如何与目标软件服务交互。</p> <h2>API 全生命周期管理</h2> <p>随着业务的增长,公司的 API 数量往往会越来越多,如果不对这些 API 进行管理的话,通常就会带来混乱,比如人员协作成本增高、服务稳定性以及安全性受到挑战。因此,人们提出了所谓 API 全生命周期管理的概念,以便可以更好地管理 API。我们可以将一个 API 从设计开始,到最后下线完成使命这个过程分成不同的阶段。通常来说,我们会将 API 生命周期分为规划和设计、实现、管理三个大阶段。</p> <h3>规划和设计</h3> <p>作为工程师,我们总是强调在编码前先进行方案的设计。API 也不例外,我们需要根据业务明确一个 API 的功能目的,然后结合相关技术栈,将业务语言翻译成技术语言。通常来说,API 规划和设计是围绕着文档进行的,以 RESTful API 为例,API 文档中应该包括如下信息:</p> <ul> <li>API 功能描述</li> <li>API 对应的 URL</li> <li>HTTP 请求方法</li> <li>请求参数、请求体、请求头的描述(以及约束)</li> <li>可能的响应状态码和响应体的描述</li> </ul> <p>关于如何撰写一份合理的 API 文档,人们也开展过很多的研究,目前比较流行的是按照 OpenAPI Specification V3 进行 API 文档的设计。</p> <p>此外,在真实世界里,API 的规划和设计往往是多人协作的,出于这样的需求,市场上也诞生了很多专注于 API 规划和设计的平台,比如 Postman。这些工具允许用户以可视化的方式设计 API,同时能满足协作的需求(也许是在它们的付费版本中),并且允许用户将 API 以某种格式进行导入导出,以便进行迁移。</p> <h3>实现</h3> <p>API 设计完毕后,工程师们就可以着手开发了。工程师可以选择自己擅长的、或者是组织要求的技术栈实现 API。并且在开发后为 API 进行测试,比如工程师可以为 API 添加端到端的测试、或者请求 QA 团队的测试。实现完毕后,工程师即可着手准备 API 的部署了。</p> <h3>管理阶段</h3> <p>相比之下,API 的管理阶段更显复杂,它包含了 API 的部署、监控、调试、安全加固。API 网关在这一阶段发挥了它巨大的作用。在部署完 API 后,我们通常不会直接将 API 所在的服务实例直接暴露,这种行为不安全也不具备可扩展性。相反,我们会通过 API 网关完成 API 的代理,API 网关会负责将 API 请求转发到真正的 API 服务,并且我们可以在 API 网关上配置相关的策略,比如限流限速(防止 API 服务过载)、认证(授权后方可访问 API)、可观测性(实时监控 API 调用状态)。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/24/JKTjbMlq_api-full-lifecycle-simple.PNG" alt="API Full Lifecycle Management" referrerpolicy="no-referrer"></p> <p>当然,API 不是一成不变的,工程师们总是需要对 API 进行功能迭代和缺陷修复,因此,在API 彻底退役之前,它将在规划和设计、实现和管理阶段来回移动。</p> <h2>API “消费”</h2> <p>API 全生命周期管理是站在 API “生产者”(API 开发者、维护者)的角度,来简化 API 的管理问题的。它并没有覆盖API 的“消费”问题,即如何让外部开发者(也可能是来自同一公司不同团队的开发者)能够方便地集成 API 的问题。我们不妨来看下,如果想让一个外部的开发者调用你的 API,需要解决哪些问题?</p> <p>第一个问题是,如何让外部的开发者查看到 API 的信息,包括 API 的接入地址、API 的描述、参数约束、使用示例等。这些详细信息能够有效地帮助到外部开发者去理解并使用 API;第二个问题是,作为 API 的“生产者”,我们往往不希望谁都可以来调用我们开发的 API,即我们希望对 API 进行有效地保护,因此外部的开发者应该仅在获得了有效的 API 凭证以后,才能真正使用 API。更重要的是,我们希望 API 的“消费”应当是尽可能自助的,从而减少沟通协作带来的成本。</p> <p>为了优化 API “消费”这个环节,人们提出了开发者门户这个概念,借此来解决上述的问题。</p> <h2>开发者门户</h2> <p>开发者门户通常分为管理端和开发者端两个站点。管理端的用户为API“生产者”(下面称为管理员),开发者端的用户为API“消费者”(下面称为开发者)。</p> <p>开发者门户管理端最核心的功能在于让管理员高效控制 API 的发布和下线,仅发布后的 API 在开发者端站点可见。当然,管理员也可以为发布的 API 添加一些策略,例如限制访问 QPS、要求鉴权等来保护发布的 API,此外,管理员还可以在这里对来自开发者端站点的请求进行审批,包括开发者账号注册、开发者希望订阅某个 API 等。某些开发者门户产品允许管理员定制开发者端站点的样式,包括替换 Logo、修改标语等。</p> <p>而开发者端是为 API“消费者”准备的,在开发者端可以看到所有由管理员发布后的 API(以及它们的细节信息),并且向管理员申请 API 的订阅;开发者可以为已经订阅的 API 创建访问所需要的凭证信息,并通过查看 API 文档来了解如何集成。</p> <p>一些开发者门户还会集成 API 调用分析,比如在管理端以开发者的视角展示过去一天某个 API 的调用量、延迟情况等,这些数据能够成为未来迭代和优化 API 的决策依据,帮助 API 变得更加完善。</p> <p>随着 API 生态系统的完善,<a href="https://www.apiseven.com/blog/what-is-api-monetization">API 货币化</a>这一概念也逐步被人们重视起来,而开发者门户则成了进行 API 货币化的良好工具,比如管理员可以为 API 创建多个订阅计划,按照不同的配额进行差异化收费,亦或是根据 API 调用次数来进行按需付费。</p> <h2>即将推出的 API7 DevPortal</h2> <p>API7.ai 致力于为用户提供最好的 API 管理服务,为此,我们正在积极筹备一款开发者门户产品,我们称之为 API7 DevPortal。这款开发者门户产品将和 <a href="https://api7.ai/enterprise">API7 Enterprise</a>(企业级 API 网关产品,基于 <a href="https://apisix.apache.org/">Apache APISIX</a> 构建)结合使用,并提供 API 订阅和开发者注册审批的功能。我们的客户可以轻松地将已经通过网关代理的 API 发布到开发者门户,进而让用户的开发者们在开发者端了解到 API 的详细信息。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/24/tXODVC03_api7-devportal.PNG" alt="API7 DevPortal" referrerpolicy="no-referrer"></p> <p>未来,API7 DevPortal 将在两方面进行深入迭代,第一是<strong>和客户的审批流打通</strong>,部分客户公司有统一的审批平台,用来管理各类审批操作,我们希望在可以在不改变客户原有审批习惯的情况下让客户使用 API7 DevPortal;第二是<strong>支持 API 货币化</strong>,帮助我们的客户为不同的开发者提供不同层级的订阅服务,并且支持收费。我们将在不久后把 API7 DevPortal 推向市场,如果你对这款产品感兴趣的话,请<a href="https://api7.ai/contact">点击这里</a>联系我们。</p> <h2>总结</h2> <p>开发者门户在管理 API 消费的环节上起到了关键的作用,它帮助 “API 生产者们”有效地解决了 API 的集成问题,且没有安全等方面的牺牲,甚至能够帮助 “API 生产者们”进行变现。在 API 大行其道的今天,也许是时候考虑在你的团队里使用开发者门户了。</p>

从1秒到10毫秒!在APISIX中减少Prometheus请求阻塞

<h2>现象</h2> <p>在 APISIX 社区中,曾有部分用户陆续反馈一种神秘现象:部分请求延迟较长。具体表现为:当流量请求进入一个正常部署的 APISIX 集群时,偶尔会出现部分请求有 1 ~ 2 秒的延迟。用户的 QPS 规模大概在 1 万,但是这种异常请求非常少见,每隔几分钟就会出现 1 ~ 3 次。一些用户在 issue 中也提供了捕获到的延迟较长的请求。从这些截图中可以看出,确实有请求延迟较高,甚至可以达到秒级别。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/31/uDLOpLOB_rGTqZMPdD2.jpeg" alt="High Latency Requests 1.png" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/2022/12/29/63ad4ff190201.png" alt="High Latency Requests 2.png" referrerpolicy="no-referrer"></p> <p>这种现象伴随着另一种现象:某个 worker 进程的 CPU 占用率达到了 100%。</p> <p><img src="https://static.apiseven.com/2022/12/29/63ad4ff52986d.png" alt="100% CPU.png" referrerpolicy="no-referrer"></p> <p>开发团队通过不同渠道与这些反馈的用户沟通得知,这个现象发生的条件是:</p> <ol> <li>开启 prometheus 插件,并且有 Prometheus Exporter 访问 APISIX 的 endpoint <code>/apisix/prometheus/metrics</code> 来采集指标;</li> <li>prometheus 插件统计的 metrics 的数量达到一定规模,通常是上万级别;</li> </ol> <p>这个现象是在业界称为 "长尾请求",是指在一个请求群体中,大部分请求响应时间较短,但有少部分请求响应时间较长的情况。它可能是由于后端系统的性能瓶颈、资源不足或其他原因导致的。它不是一个致命的 bug,但是它严重影响了终端用户的体验。</p> <h2>抽丝剥茧</h2> <p>APISIX 基于一个开源的 Lua 库 <a href="https://github.com/knyar/nginx-lua-prometheus">nginx-lua-prometheus</a> 开发了 Prometheus 插件,提供跟踪和收集 metrics 的功能。当 Prometheus Exporter 访问 APISIX 暴露的 Prometheus 指标的 endpoint 时,APISIX 会调用 nginx-lua-prometheus 提供的函数来暴露 metrics 的计算结果。</p> <p>开发团队从社区用户,企业用户等渠道收集汇总了长尾请求发生的条件,基本定位了问题所在:nginx-lua-prometheus 中用于暴露 metrics 指标的函数 <code>prometheus:metric_data()</code>。</p> <p>不过这只是初步推断,还需要直接的证据来证明长尾请求与此有关,并且需要搞清楚以下问题:</p> <ol> <li>这个函数具体做了什么?</li> <li>这个函数为什么会造成长尾请求现象?</li> </ol> <p>开发团队构造了本地复现环境,这个复现环境主要模拟以下场景:</p> <ol> <li>模拟客户端发送正常请求,被 APISIX 代理到上游</li> <li>模拟 Prometheus Exporter 每隔 5 秒访问 <code>/apisix/prometheus/metrics</code>,触发 APISIX 运行 <code>prometheus:metric_data()</code> 函数</li> </ol> <p>复现环境示意图:</p> <p><img src="https://static.apiseven.com/2022/12/29/63ad4ff72f99a.png" alt="Reproduced Environment.png" referrerpolicy="no-referrer"></p> <p>在执行复现测试时,我们会观察 wrk2 的测试结果中的 P100 等指标来确认是否发生了长尾请求现象,并且会对运行中的 APISIX 生成火焰图,来观测发生长尾请求时,CPU 资源消耗在哪里。</p> <p>wrk2 的测试结果如下:</p> <pre><code class="language-text"> Latency Distribution (HdrHistogram - Uncorrected Latency (measured without taking delayed starts into account)) 50.000% 1.13ms 75.000% 2.56ms 90.000% 4.82ms 99.000% 14.70ms 99.900% 27.95ms 99.990% 74.75ms 99.999% 102.78ms 100.000% 102.78ms </code></pre> <p>根据这个测试结果可以得到结论:在测试期间,99% 的请求在 14.70 毫秒内完成了,但是还有很少一部分请求消耗了 100 多毫秒。并且我们用 metrics 数量作为变量,进行了多次测试,发现 metrics 数量与 P100 的延迟呈线性增长。如果 metrics 达到 10 万级别,P100 将达到秒级别。</p> <p>生成的火焰图如下:</p> <p><img src="https://static.apiseven.com/2022/12/29/63ad4ff9130d0.png" alt="Flame Graph1" referrerpolicy="no-referrer"></p> <p>从火焰图的函数堆栈可以看到,<code>prometheus:metric_data()</code> 占据了最长的横轴宽度,这证明了大量 CPU 消耗在这里。这也直接证明了 <code>prometheus:metric_data()</code> 造成长尾请求现象。</p> <p>下面我们来简单分析一下 <code>prometheus:metric_data()</code> 函数做了什么。<code>prometheus:metric_data()</code> 将会从共享内存中获取指标,对指标进行分类,并加工成 Prometheus 兼容的文本格式。在这个过程中,会对所有 metrics 按照字典序进行排序,会用正则处理 metrics 的前缀。根据经验,这些都是非常昂贵的操作。</p> <h2>不够完美的优化</h2> <p>当定位到有问题的代码后,下一步就是结合火焰图,详细分析代码,寻找优化空间。</p> <p>从火焰图可以定位到 <code>fix_histogram_bucket_labels</code> 函数。通过 review 这个函数,我们发现了两个比较敏感的函数:<code>string:match</code> 和 <code>string:gsub</code>。这两个函数都不能被 LuaJIT 所 JIT 编译,只能解释执行。</p> <p>LuaJIT 是一个针对 Lua 编程语言的 JIT 编译器,可以将 Lua 代码编译成机器码并运行。这相比于使用解释器来运行 Lua 代码,可以提供更高的性能。 使用 LuaJIT 运行 Lua 代码的一个优势是,它可以大幅提升代码的执行速度。这使得 APISIX 在处理大量请求时可以保持较低的延迟,并且可以在高并发环境下表现出较好的性能。 关于 LuaJIT 的更多介绍可以参考:<a href="https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/what_jit.html">什么是 JIT</a>?</p> <p>因此不能被 LuaJIT 编译的代码必然会成为潜在的性能瓶颈。</p> <p>我们整理以上信息并提交了 <a href="https://github.com/knyar/nginx-lua-prometheus/issues/130">issue: optimize the long-tail request phenomenon</a> 到 nginx-lua-prometheus,与这个项目的作者 knyar 一起探讨可以优化的空间。knyar 响应很及时,我们沟通后明确了可以优化的点。于是提交了 PR:<a href="https://github.com/knyar/nginx-lua-prometheus/pull/131">chore: use ngx.re.match instead of string match to improve performance</a> 进行优化。 在这个 PR 中,主要完成了:</p> <ul> <li>用 <code>ngx.re.match</code> 替代 <code>string:match</code></li> <li>用 <code>ngx.re.gsub</code> 替代 <code>string:gsub</code></li> </ul> <p>在完成这个优化后,我们其实非常理性地知道,这个优化只能提升一些性能,但不能根本解决问题。根本问题是:</p> <p>Nginx 是一种多进程单线程的架构。所有的 worker 进程都会监听 TCP 连接,但一旦连接进入了某个 worker 进程,就不能再被迁移到其他 worker 进程去处理了。 这意味着,如果某个 worker 进程非常忙碌,那么该 worker 进程内的其他连接就可能无法及时获得处理。另一方面,进程内的单线程模型意味着,所有 CPU 密集型和 IO 密集型的任务都必须按顺序执行。如果某个任务执行时间较长,那么其他任务就可能被忽略,导致任务处理时间不均匀。</p> <p><code>prometheus:metric_data()</code> 占据了大量的 CPU 时间片进行计算,挤压了处理正常请求的 CPU 资源。这也是为什么会看到某个 worker 进程的 CPU 占用率达到 100%。</p> <p>基于这个问题,我们在完成上述优化后继续分析,抓取了火焰图:</p> <p><img src="https://static.apiseven.com/2022/12/29/63ad4ffa4e070.png" alt="Flame Graph2" referrerpolicy="no-referrer"></p> <p>上面火焰图 <code>builtin#100</code> 表示的是 luajit/lua 的库函数(比如 <code>string.find</code> 这种),可以通过 https://github.com/openresty/openresty-devel-utils/blob/master/ljff.lua 这个项目里的脚本来得到对应的函数名称。</p> <p>使用方式:</p> <pre><code class="language-shell">$ luajit ljff.lua 100 FastFunc table.sort </code></pre> <p>由于计算 metrics 时占用了过量的 CPU,所以我们考虑在计算 metrics 时适当让出 CPU 时间片。</p> <p>对于 APISIX 来说,处理正常请求的优先级是最高的,CPU 资源应当向此倾斜,而 <code>prometheus:metric_data()</code> 只会影响 Prometheus Exporter 获取指标时的效率。</p> <p>在 OpenResty 世界,有一个隐秘的让出 CPU 时间片的方式:<code>ngx.sleep(0)</code> 。我们在 <code>prometheus:metric_data()</code> 中引入这种方式,当处理所有的 metrics 时,每处理固定数目(比如 200 个)的 metrics 后让出 CPU 时间片,这样新进来的请求将有机会得到处理。</p> <p>我们提交了引入这个优化的 PR:<a href="https://github.com/knyar/nginx-lua-prometheus/pull/139">feat: performance optimization</a>。</p> <p>在我们的测试场景中,当 metrics 的总数量达到 10 万级别时,引入这个优化之前用 wrk2 测试得到的结果:</p> <pre><code class="language-text"> Latency Distribution (HdrHistogram - Uncorrected Latency (measured without taking delayed starts into account)) 50.000% 10.21ms 75.000% 12.03ms 90.000% 13.25ms 99.000% 92.80ms 99.900% 926.72ms 99.990% 932.86ms 99.999% 934.40ms 100.000% 934.91ms </code></pre> <p>引入这个优化后,用 wrk2 测试得到的结果:</p> <pre><code class="language-text"> Latency Distribution (HdrHistogram - Uncorrected Latency (measured without taking delayed starts into account)) 50.000% 4.34ms 75.000% 12.81ms 90.000% 16.12ms 99.000% 82.75ms 99.900% 246.91ms 99.990% 349.44ms 99.999% 390.40ms 100.000% 397.31ms </code></pre> <p>可以看到 P100 的指标大约是优化前的 1/3 ~ 1/2。</p> <p>不过这并没有完美解决这个问题,通过分析优化后的火焰图:</p> <p><img src="https://static.apiseven.com/2022/12/29/63ad4ffa4e070.png" alt="Flame Graph After Optimization" referrerpolicy="no-referrer"></p> <p>可以直接看到 <code>builtin#100</code>(即 <code>table.sort</code>) 和 <code>builtin#92</code>(即 <code>string.format</code>)等,仍然占据了相当宽度的横轴,这是因为:</p> <ol> <li><code>prometheus:metric_data()</code> 中首先会对所有的 metrics 调用 <code>table.sort</code> 进行排序,当 metrics 到 10 万级别时,相当于对 10 万个字符串进行排序,并且 <code>table.sort</code> 不可以被 <code>ngx.sleep(0)</code> 中断。</li> <li>使用 <code>string.format</code> 的地方,以及 <code>fix_histogram_bucket_labels</code> 无法优化,经过与 knyar 交流后得知,这些步骤必须存在以保证 <code>prometheus:metric_data()</code> 可以产出格式正确的 metrics。</li> </ol> <p>至此,代码层面的优化手段已经用完了,但遗憾的是,还是没有完美解决问题。P100 的指标仍然有明显的延迟。</p> <h2>怎么办?</h2> <p>让我们再回到核心问题:<code>prometheus:metric_data()</code> 占据了大量的 CPU 时间片进行计算,挤压了处理正常请求的 CPU 资源。</p> <p>在 Linux 系统中,CPU 分配时间片的单位是线程还是进程?准确来说是线程,线程才是实际的工作单元。不过 Nginx 是多进程单线程的架构,实际在每个进程中只有一个线程。</p> <p>此时我们会想到一个优化方向:将 <code>prometheus:metric_data()</code> 转移到其他线程,或者说进程。于是我们调研了两个方向:</p> <ol> <li>用 <code>ngx.run_worker_thread</code> 来运行 <code>prometheus:metric_data()</code> 的计算任务,相当于将 CPU 密集型任务交给线程池;</li> <li>用单独的进程来处理 <code>prometheus:metric_data()</code> 的计算任务,这个进程不会处理正常请求。</li> </ol> <p>经过 PoC 后,我们否定了方案 1,采用了方案 2。否定方案 1 是因为 <code>ngx.run_worker_thread</code> 只适合运行与请求无关的计算任务,而 <code>prometheus:metric_data()</code> 明显是与请求有关的。</p> <p>方案 2 的实现:让 <code>privileged agent</code>(特权进程)来处理 <code>prometheus:metric_data()</code>。但是特权进程本身不监听任何端口,也不会处理任何请求。因此,我们需要对特权进程进行一些改造,让它监听端口。</p> <p>最终,我们通过 <a href="https://github.com/api7/apisix-nginx-module/pull/71">feat: allow privileged agent to listen port</a> 和 <a href="https://github.com/apache/apisix/pull/8434">feat(prometheus): support collect metrics works in the priviledged agent</a> 实现了方案 2。</p> <p>我们使用带上了这个优化的 APISIX 来测试,发现 P100 的指标延迟已经降低到合理的范围内,长尾请求现象也不存在了。</p> <pre><code class="language-text"> Latency Distribution (HdrHistogram - Uncorrected Latency (measured without taking delayed starts into account)) 50.000% 3.74ms 75.000% 4.66ms 90.000% 5.77ms 99.000% 9.99ms 99.900% 13.41ms 99.990% 16.77ms 99.999% 18.38ms 100.000% 18.40ms </code></pre> <p>这个方案有些巧妙,也解决了最核心的问题。我们在生产环境中观察并验证了这个方案,它消除了长尾请求现象,也没有造成其他额外的异常。 与此同时,我们发现社区中也有类似的修复方案,有兴趣的话可以延伸阅读:<a href="https://heapdump.cn/article/4822247">如何修改 Nginx 源码实现 worker 进程隔离</a>。</p> <h2>展望</h2> <p>在我们修复这个问题的时候,产生了一个新的思考:nginx-lua-prometheus 这个开源库适合 APISIX 吗?</p> <p>我们在 APISIX 侧解决了 <code>prometheus:metric_data()</code> 的问题,同时,我们也发现了 nginx-lua-prometheus 存在的其他问题,并且修复了。比如<a href="https://github.com/knyar/nginx-lua-prometheus/pull/151">修复内存泄漏</a>,以及修复 <a href="https://github.com/knyar/nginx-lua-prometheus/pull/147">LRU 缓存淘汰</a>。</p> <p>nginx-lua-prometheus 刚开始是被设计为 Nginx 使用,并不是为了 OpenResty 以及基于 OpenResty 的应用所设计的。OpenResty 生态内没有比 nginx-lua-prometheus 更成熟的对接 Prometheus 生态的开源项目,因此 nginx-lua-prometheus 不断被开源社区的力量推动成为适合 OpenResty 生态的方向。</p> <p>也许我们应该将视野放得更开阔一些,寻找不用修改 APISIX 底层的方式来对接 Prometheus 生态。比如设计一个更适合 APISIX 的依赖库,或者用某种方式对接 Prometheus 生态中成熟的项目,将收集和计算 metrics 的过程转移到那些成熟的项目中。</p>

备战一年半,我们让最火的开源网关上了云

<p>这是最好的时代,我们满怀信心施展才华;这也是最坏的时代,我们遇到了前所未有的竞争。工程师们从不畏惧困难,因为热爱能化解一切困难。本文源于对张超(API7 Cloud 团队负责人,Apache APISIX PMC member)的采访,这是一个关于 API7 Cloud 诞生的故事,路转峰回,寻寻觅觅。一年半后,我们舒颜感叹:莫愁千里路,自有到来风!</p> <h2>一款优秀的产品只需要一个契机</h2> <p>云原生时代风云变幻,开源产品层出不穷。</p> <p>2019 年 APISIX 在温铭和院生的代码下诞生,6 月当时仍处于 Demo 阶段的 APISIX 便在 GitHub 上开源。一经开源,APISIX 的独特性让它迅速席卷开源领域。2019 年 8 月,APISIX 成功进入了 Apache 孵化器,次年 7 月 APISIX 顺利毕业,并成为了 Apache 软件基金会的顶级开源项目。</p> <p>Apache APISIX 的诞生打响了 API7.ai 商业化版图的“第一枪”。API7.ai 基于对市场发展的理解,定位 SaaS 未来能成为发展的方向,能够真正给公司带来增长,开始投入商业化。<strong>我们 API7 Cloud 团队的负责人张超表示,“我们也非常希望能够基于 Apache APISIX 这样一个优秀的 API 网关去做一款 SaaS 产品”。</strong></p> <p>在发现这个机会之后,我们很快进入了调研阶段。2021 年 6-7 月,API7 Cloud 完成了最初的提议,下半年定了一个 MVP(最简化可实行产品),随即工程师们乐此不疲地投入开发。一方面,我们逐步调整产品的稳定性和可用性;另一方面,不断地往 MVP 里加一些新功能。后来又找到了一些试用客户,和客户一起使用并打磨产品。</p> <p>API7 Cloud 的出现为云原生时代多云与混合云场景下管理 API 提供了最佳实践,这么一款优秀的产品背后,是一群热血的年轻人,撑起了 SaaS 产品新理念的一片天。</p> <h2>一群热血且以用户为中心的工程师</h2> <p>正如团队负责人的飞书签名所言:“I wanna create a great SaaS product”,简短的一句话背后是无数个勤勤恳恳加班加点的日夜。</p> <p>这一路上,我们同甘也共苦,累并快乐着!</p> <p><strong>我们 API7 Cloud 工程师团队几乎都是 Apache Software Foundation 顶级开源项目的 committer 和 PMC member</strong>,因而都对 APISIX 有很深入的了解,尤其是理解了技术最底层核心的部分。我们的成员同时都是<strong>开源爱好者</strong>,曾在快手、阿里巴巴、有赞等国内知名企业担任过重要的角色。对开源的热情把我们汇聚在 API7.ai,而对产品和用户的热忱是我们源源不断的动力。</p> <p>在产品投入市场初期,我们团队发现了一个比较严重的设计问题,导致在加入一些和 APISIX 相关的新功能时,发现在技术上改动的工作量、难度以及兼容性上都遇到了很大的挑战。当时用户已经在使用,为了不影响用户的正常使用,我们引入了一个新的方案:<strong>在非工作日的晚上执行发布,错开用户使用的时间。该改进持续了两个月左右,其中涉及 3-4 次发布。</strong></p> <p><strong>我们注重用户体验。<strong>平时在用户群里,API7 Cloud 工程师们积极响应,跟进用户的问题。但凡遇到安全问题,便会立即投入人力收紧问题,遇到非安全的问题也会控制在两周内收紧,不过 API7 Cloud 目前为止还未出现安全性的问题。每周工程师们会和客户沟通使用的体验,以不断改进。另外,我们团队成员</strong>还是写文章的好手</strong>,他们自发组织撰写介绍 API7 Cloud 亮点的文章,以向更多用户提供专业的服务。</p> <h2>不平坦,才更有成就感</h2> <p>在项目中,我们也遇到过很多难点。</p> <p>我们团队认为:“<strong>产品如何定价</strong>是一个难点,对此我们也一直在探索,探索对产品的理解和市场的理解,同时结合用户的反馈,为未来制定合理的定价策略,综合多维度去考虑。”</p> <p>要想打开海外市场,还需要满足数据主权的管理要求。例如,欧盟制定了 GDPR(General Data Protection Regulation),即《通用数据保护条例》,在欧美市场进行贸易却不满足该规定将面临巨额的赔偿。在数据主权上,我们做了很多的努力。所幸的是,<strong>API7 Cloud 本身在数据主权上有一定的积累,客户也用 APISIX 解决过类似的问题</strong>。因此,API7 基于用户的需要,制定了一套满足需要的方案,逐渐立足于国际市场。</p> <p><strong>此外,我们重视产品在用户数据隐私性方面的保护</strong>。2022 年下半年我们投入了很多时间在保障用户数据安全方面,目前已正式通过 SOC 2 Type 1 审计。该审计报告能有效说明<strong>API7 Cloud 在安全性、可用性和保密性上的能力</strong>,其中包括对核心系统、账号管理、变更审批等等方面的管理,例如:备份、容量规划、权限回收。</p> <p>API7 Cloud 团队的工程师们在整个过程中投入了很多精力,有效地推动了制度落地,可谓是能文能武!</p> <h2>多云与混合云场景下的 API 管理利器</h2> <p>在最初的产品定位中,我们考虑到<strong>多云和混合云的场景下统一管理 API 的使用</strong>将会是一个巨大的挑战,因而 API7 Cloud 的定位就是帮助企业解决多云和混合云场景下的问题。它的目标用户是那些业务上了云,且需要一款 API 管理工具的用户;或者不仅仅是上了云,而且是使用了多云或者混合云的用户。</p> <p><strong>API7 Cloud 基于 Apache APISIX,围绕着 Apache APISIX 所提供的功能进行了产品化,旨在让用户更简单、更放心地配置和使用 APISIX</strong>。市面上有很多类似的产品,例如:Kong Konnect、Tyk Cloud、Mulesoft Anypoint Platform 和 Amazon API Gateway。但 API7 Cloud 集成了 APISIX 的优势,这些是 API7 Cloud 独有的,例如:</p> <ul> <li> <p><strong>基金会品牌</strong>:无品牌纠纷,实力过硬</p> </li> <li> <p><strong>高性能</strong>:APISIX 的 QPS 能达到 23,000,平均延迟仅 0.6 毫秒</p> </li> <li> <p><strong>社区活跃</strong>:APISIX 社区响应快,迭代更新速度快</p> </li> <li> <p><strong>生态强大</strong>:支持近 100 个插件,生态包容</p> </li> </ul> <p>API7 Cloud 以 Apache APISIX 为基础,又对它进行了更企业化的定制。</p> <ul> <li> <p><strong>支持开源 APISIX 的所有插件功能</strong></p> </li> <li> <p><strong>强化了开源 APISIX 的动态能力</strong>,比如支持了动态的服务发现功能</p> </li> <li> <p><strong>开放 API 且提供 SDK,允许自动化的 API 管理</strong>,允许通过程序集成 API7 Cloud 实现自动化</p> </li> </ul> <p>在 2023 年即将支持 APISIX Gateway API 规范,未来允许用户在开源 APISIX、企业版以及 API7 Cloud 之间相互切换。由此一来,为用户节约数据迁移的成本。</p> <p><a href="https://www.apiseven.com/blog/api7.ai/try" title="API7 Cloud"><img src="https://static.apiseven.com/uploads/2023/03/03/9GeX4mIq_image%20%281%29.png" alt="API7 Cloud" title="API7 Cloud" referrerpolicy="no-referrer"></a></p> <p>在技术设计层面,API7 Cloud 特色鲜明,提供了诸多功能,例如:</p> <ul> <li> <p><strong>API 管理</strong>:提供 API 管理、SSL 证书管理、灰度发布、精细化路由等多种功能,并能通过导入 OpenAPI 文档创建 API</p> </li> <li> <p><strong>可观测性</strong>:API7 Cloud 聚合延迟、QPS、HTTP 状态码分布、HTTP 请求成功率等状态信息,通过 Cloud 产品界面可视化,让客户可以及时发现系统中的潜在问题</p> </li> <li> <p><strong>无供应商锁定</strong>:客户的服务可部署在任何云环境或本地。此外,API7 Cloud 与 Apache APISIX 百分百兼容,开源用户也可以零成本迁移项目到云端,无需担心供应商锁定</p> </li> <li> <p><strong>全方位的安全防护和隐私合规</strong>:所有的通信和数据传输都是通过 mTLS 协议进行加密。存储在 API7 Cloud 上的数据,也有身份认证、鉴权和审计功能的保护,符合 GDPR 的合规要求</p> </li> </ul> <p>以及即将在 2023 年实现的支持<strong>多集群管理</strong>以及<strong>数据主权</strong>的功能。</p> <p><strong>API7 Cloud 另一个优势在于它使用简单易上手</strong>。如“UML之父”Grady Booch 所说:“好的软件之所以好,是因为它化繁为简。”用户在使用 API7 Cloud时,在注册登录后按照指引仅需在三步内完成网关实例的搭建,即可进行 API 管理,包括灰度发布、添加限流策略、添加认证等。</p> <h2>前路漫漫亦灿灿</h2> <p>API7.ai 是一家致力于支持 API 管理和分析的开源软件基础设施公司,它为微服务和实时流量处理提供广泛的产品,例如 API 网关、Kubernetes Ingress Controller 和服务网格。API7 Cloud 是 API7.ai 推出的中心化 API 管理平台,它在多云和混合云的场景下统一管理 API 中发挥了独特的作用。</p> <p>未来我们会在 <strong>API 资产管理、API 数据主权、API 分析</strong>上发力,希望帮用户更好地感知他们 API 的使用以及感知用户的客户是如何使用他们的 API 的,让他们能有一个非常直观的感受。同时基于 API 分析以及 API 的一些能力,希望能给用户做一些<strong>API 上的预测</strong>,从而提前帮助用户做<strong>容量规划</strong>和<strong>风险预警</strong>。</p> <p>请期待我们 API7 Cloud 为用户带来更多新的体验,我们也期待更多用户能切身体会到 API7 Cloud 的实力!</p> <p>扫码一键注册 API7 Cloud,<a href="https://console.api7.cloud/signup?ref=baidu">即刻试用</a>!</p> <p><img src="https://static.apiseven.com/uploads/2023/03/03/Ex84S051_https___console.api7.cloud_signup_ref=baidu%20%281%29.png" alt="Try" referrerpolicy="no-referrer"></p> <p>更多产品试用:<a href="https://api7.ai/try">api7.ai/try</a></p>

API Gateway vs Load Balancer:选择适合你的网络流量管理组件

<p>由于互联网技术的发展,网络数据的请求数节节攀升,这使得服务器承受的压力越来越大。在早期的系统架构中,通常使用 Load Balancer 来将网络流量平摊到多个服务器中,以此减轻单台服务器的压力。但是现如今,后端服务的种类在不断地变多,每个种类的后端都以 API 的形式对外暴露,这使得 API 的数量也在不断变多。以传统的 Load Balancer 为主的系统架构的局限性就变得明显起来,因为它主要工作在四层,在七层上功能较弱,于是一款主要工作在七层且具有丰富扩展能力的基础设施便应运而生,它就是 API Gateway。</p> <p>在本文中,我们将介绍 Load Balancer 和 API Gateway 的功能特点,并探讨它们之间的区别,帮助读者更好地了解这两者之间的关系。</p> <h2>一、什么是 Load Balancer</h2> <p><img src="https://static.apiseven.com/uploads/2023/02/06/npYlQINW_1.png" alt="what is load balancer" referrerpolicy="no-referrer"></p> <p>Load Balancer 的主要作用是为多个后端服务提供负载均衡功能,依据不同的负载均衡算法让这些服务可以分摊流量。Load Balancer 的历史非常悠久,从演进路径上看大致可以分为以下这几个阶段:</p> <ul> <li> <p>第一阶段:这一阶段的 Load Balancer 通常由硬件设备组成,具有高性能、高可靠性的特点,但灵活性较差,价格昂贵。比较典型的是 F5 这种基于硬件的 Load Balancer 。</p> </li> <li> <p>第二阶段:Load Balancer 开始以软件形式实现,使其更加灵活和可扩展,通常以软件分发的形式出现,因此价格也比较低廉,比如 LVS 就属于这一类。</p> </li> <li> <p>第三阶段:随着云计算技术的兴起,Load Balancer 也开始有了云版本,这个版本的 Load Balancer 其中一个好处是可以帮助企业以更低的成本获得高性能的负载均衡服务,另一个好处是它能够利用云计算的可扩展性和弹性的特点来提高整体可用性。例如 AWS 的 Classic Load Balancer、Application Load Balancer、Network Load Balancer 等。</p> </li> </ul> <p>Load Balancer 除了用于分摊流量、提高网络的伸缩性外,还可以用于提升网络安全。比如可以将内网服务器与外网进行隔离,防止互联网的恶意攻击和访问。一个简单的使用场景就是,一个包含敏感信息的内部服务器,Load Balancer 可以把内部服务器隔离在内网中,这样就能有效保护内部服务器的安全。</p> <h2>二、什么是 API Gateway</h2> <p><img src="https://static.apiseven.com/uploads/2023/02/06/PUDzqJ5k_what-is-api-gateway.png" alt="what is api gateway" referrerpolicy="no-referrer"></p> <p>API Gateway 简单来说是一种主要工作在七层,专门用于 API 的管理和流量转发的基础设施,并在此基础上拥有 Load Balancer 所不具备的强大的扩展性,比如:认证、可观测性、自定义插件等等。简单来说,包括但不限于以下这些特点:</p> <ul> <li> <p>丰富的路由策略:API Gateway 工作在七层,所以它可以解析到 HTTP/HTTPS 层的数据。因此它可以根据请求的 Path 或 Domain 甚至是 Header 作为条件,将请求转发到不同的上游服务器。</p> </li> <li> <p>认证:可以在API层面支持多种多样的认证方式来避免非法请求,比如 OAuth2、JWT 等等,直接将认证这部分服务独立出来,不侵入或者少侵入业务代码。</p> </li> <li> <p>限流:支持对不同程度的路由进行细粒度的限流,防止恶意攻击,防止后端服务雪崩。</p> </li> <li> <p>可观测性:可观察性是指从系统外部观察系统内部程序的运行状态和资源使用情况的能力。 API Gateway 支持将日志对接到 Kafka、 Google Cloud Logging Service、Elasticsearch 等,支持将相关 metrics 接入到 prometheus、datadog 等。</p> </li> <li> <p>扩展:因为 API Gateway 自身是网关身份,这就注定对它要求是能适配各家公司不同应用场景,比如不同的鉴权、灰度、安全策略、日志收集等,必须允许用户自由选择所需扩展或者自定义开发,因此扩展性很强,允许选择的扩展种类也十分丰富。以 Apache APISIX 举例,光是认证的扩展就有 13 款,几乎涵盖了市面上常见的认证需求。</p> </li> </ul> <p>目前市面上有许多 API Gateway,比如 Apache APISIX、Kong、Tyk、Zuul 等,开发者可以根据自己的需求选择合适的 API Gateway。</p> <h2>三、API Gateway 与 Load Balancer 主要区别</h2> <p><img src="https://static.apiseven.com/uploads/2023/02/06/roVkkRLu_api-gateway-vs-lb-1.png" alt="api gateway different with load balancer img 1" referrerpolicy="no-referrer"></p> <p>首先,他们主要工作的侧重点不同。虽然说 API Gateway 和 Load Balancer 都支持四层和七层的代理。但是,API Gateway 主要侧重于七层,而 Load Balancer 主要侧重于四层。</p> <p>工作在四层的 Load Balancer 拥有许多有利的特点,首先是它相比于 API Gateway 减少了协议解析的损耗,具有更强的吞吐能力。其次就是它支持透传客户端 IP 地址,而 API Gateway,一般是通过 HTTP 头方式传递客户端 IP 地址。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/06/S25JMIe9_api-gateway-vs-lb-2.png" alt="api gateway different with load balancer img 2" referrerpolicy="no-referrer"></p> <p>其次就是功能的丰富程度不同。Load Balancer 的 HTTP 七层处理能力比较弱,往往不包含认证、授权、鉴权、复杂路由逻辑、日志收集等功能。API Gateway 则具有相当强大的七层协议处理能力,可以在此基础上,附加各种各样的功能扩展,比如权限控制、日志、API 管理、Serverless 等等。</p> <p>现如今,科技公司的产品需求变幻莫测,对于很多公司来说支持自定义开发是刚需。API Gateway 支持各式的自定义开发,比如支持丰富的编程语言,支持在流量转发的不同阶段注入自定义的处理逻辑,而 Load Balancer 基本不支持任何自定义功能开发。</p> <p>还有一点就是 Load Balancer 通常采用流量直接分发的形式做负载均衡,它通过算法将流量数据直接发向某个后端服务器节点。这意味着后端等待接收流量的每一个服务实例行为都必须是一致的,这减少了一定的灵活性。而 API Gateway 则是以 URL Path 、Domain、Header 等维度进行流量分发,后端等待接收流量的服务实例可以多种多样,可以是某个 Private API,也可以是某个 gRPC 的 API。这就使流量分发变得十分地灵活。</p> <h2>四、使用场景</h2> <h3>微服务场景</h3> <p><img src="https://static.apiseven.com/uploads/2023/02/06/njZDrvwk_microservice-screen.png" alt="in microservice screen" referrerpolicy="no-referrer"></p> <p>API Gateway 对于微服务架构的系统是刚需。首先它可以方便地管理和路由多种不同的后端服务,其次可以提供许多高级功能,比如身份验证、授权、限流、转发、日志记录等功能。这样不同的微服务之间无需重复实现限流、认证等功能,让微服务的每个服务的功能实现更加纯粹,减少研发成本。</p> <p>由于微服务的特点是服务种类多,工作在四层的 Load Balancer 不太适合对种类繁多后端服务做负载均衡,它更适合用于单体后端服务。即使是工作在七层的 Load Balancer,因为一般不能提供较为丰富的高级功能,相比于 API Gateway 在微服务上优势也不明显。</p> <h3>API 管理与发布</h3> <p>在需要对大量的 API 进行管理和发布的场景,API Gateway 也非常适用,因为它具有强大的 API 管理功能,可以让你随时随地让某个 API 上线或者下线,快速地修改 API 转发的配置,快速地为某个 API 添加限流、认证、日志等等功能而无需重新启动 API Gateway。</p> <p>以 Apache APISIX 为例,Apache APISIX 是 Apache 基金会旗下的顶级开源项目,也是当前最活跃的开源网关项目。作为一个动态、实时、高性能的开源 API 网关,Apache APISIX 提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/10/rMQQXaaA_apisix-dashboard-router.png" alt="apisix dashboard screenshot image 1" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/uploads/2023/02/06/rYEyFHcM_apisix-dashboard2.png" alt="apisix dashboard screenshot image 12" referrerpolicy="no-referrer"></p> <p>而传统的 Load Balancer 则在 API 管理上较为弱势,不具备如此丰富的高级功能。</p> <h3>高性能的网络出入口</h3> <p>对于需要大流量、极高稳定性的网络出入口的场景,工作在四层的 Load Balancer 显然更为适用。它可以把网络原始四层流量直接分发到各个后端服务中,不存在中间层多次解析应用层协议的影响,具有更强的吞吐能力。</p> <p>而工作在七层的 API Gateway 作为统一的入口,会由于需要解析协议,存在一定的吞吐量限制。即使是使用四层的 API Gateway 来做网络出入口也不太有优势,因为这一层不是 API Gateway 的侧重点,相比于 Load Balancer 多年在这一层的技术累计,API Gateway 优势也不明显。</p> <h2>四、总结</h2> <p>总的来说,API Gateway 和 Load Balancer 是分别用于解决不同层面问题的基础设施。API Gateway 主要用于作为后端的 API 接口代理,提供对外访问不同种类 API 的一个单独入口,并且可以提供独立于后端服务的限流、认证、监控等功能;而 Load Balancer 则主要用于四层流量分发,它可以将请求分摊到多台后端服务器上,平衡后端的请求负载,以提高系统的整体可用性和容错性。</p> <p>在合理的架构设计下,一般都将 API Gateway 和 Load Balancer 配合使用,使用 Load Balancer 作为整个系统的网络出入口,将流量分发到多个 API Gateway 实例,然后每个 API Gateway 实例分别对请求进行路由、认证、鉴权等操作,这样可以使得整个网络更加稳健、可靠、可扩展。</p>

微服务为什么要用到 API 网关?

<h2>什么是微服务</h2> <p>微服务架构(通常简称为微服务)是指开发应用所用的一种架构形式。通过微服务,可将大型应用分解成多个独立的组件,其中每个组件都有各自的责任领域。在处理一个用户请求时,基于微服务的应用可能会调用许多内部微服务来共同生成其响应。微服务是互联网业务发展的结果,互联网业务的飞速发展导致系统的架构也在不断地发生变化,总体来说,系统的架构大致经历了:单体应用架构—&gt; SOA 架构—&gt;微服务架构的演变,具体发展历程和各自的优缺点如下表所示。</p> <table> <thead> <tr> <th>架构类型</th> <th>简介</th> <th>优点</th> <th>缺点</th> </tr> </thead> <tbody> <tr> <td>单体应用架构</td> <td>将所有的功能代码打包成一个服务。</td> <td>1. 架构简单,项目开发和维护成本低。</td> <td>所有模块耦合在一起,比较有利于小型项目的开发和维护;但是,对于大型项目来说却存在问题,比如:<br> 1. 项目各模块之间过于耦合,一个模块的性能问题可能导致整个项目的不可用;<br> 2. 项目的扩展性差。</td> </tr> <tr> <td>SOA 架构</td> <td>中文意思为 “面向服务的架构”,通常包含多个服务,<br>一个服务通常以独立的形式存在于操作系统进程中,服务之间通过相互依赖或者通过通信机制来完成通信的,<br>最终提供一系列的功能。</td> <td>1. 系统集成:站在系统的角度,解决企业系统间的通信问题,把原先散乱、无规划的系统间的网状结构,梳理成规整、可治理的系统间星形结构;<br> 2. 系统的服务化:站在功能的角度,把业务逻辑抽象成可复用、可组装的服务,通过服务的编排实现业务的快速再<br> 3. 业务的服务化:站在企业的角度,把企业职能抽象成可复用、可组装的服务。</td> <td>1. 服务的中心化,各服务之间存在依赖关系,如果某个服务出现故障可能会造成服务的雪崩;<br> 2. 服务之间的依赖与调用关系复杂,测试部署的困难比较大。</td> </tr> <tr> <td>微服务架构</td> <td>微服务是在 SOA 上做的升华。微服务架构重点强调的一个是"业务需要彻底的组件化和服务化",<br>原有的单个业务系统会拆分为多个可以独立开发、设计、<br>运行的小应用。各个小应用之间,相互去协作通信,来完成一个交互和集成,这就是微服务架构。</td> <td>1. 去中心化;<br> 2. 通过服务实现组件化;<br> 3. 按业务能力来划分服务和开发团队;<br> 4. 基础设施自动化( Devops、自动化部署)。</td> <td>1. 开发的成本比较高;<br> 2. 会引发服务的容错性问题;<br> 3. 会引发数据的一致性问题;<br> 4. 会涉及分布式事务。</td> </tr> </tbody> </table> <p>因此,微服务是互联网发展的必然结果,很多传统公司的系统架构也在逐步微服务化。但是,随着互联网业务的发展,API 的数量也在剧增,使用网关对API统一管理也将面临挑战,选择一个更强大的 API 网关,可以有效地增强系统的监控、容灾、鉴权和限流等能力。</p> <h2>什么是 API 网关</h2> <p>API 网关为客户与服务系统之间的交互提供了统一的接口,也是管理请求和响应的中心点,选择一个适合的 API 网关,可以有效地简化开发并提高系统的运维与管理效率。 API 网关在微服务架构中是系统设计的一个解决方案,用来整合各个不同模块的微服务,统一协调服务。API 网关作为一个系统访问的切面,对外提供统一的入口供客户端访问,隐藏系统架构实现的细节,让微服务使用更为友好;并集成了一些通用特性(如鉴权、限流、熔断),避免每个微服务单独开发,提升效率,使系统更加标准化,比如身份验证、监控、负载均衡、限流、降级与应用检测等功能。</p> <h2>为什么微服务需要 API 网关</h2> <p><img src="https://static.apiseven.com/uploads/2023/01/30/dSfRMQcD_%E5%9B%BE1.png" alt="Architecture Diagram" referrerpolicy="no-referrer"></p> <p>如上图所示,API 网关作为客户端和微服务的中间层,它可以将微服务以统一的地址对外提供服务,将外部访问这个地址的流量,根据适当的规则路由到内部集群中正确的服务节点之上,如果没有 API 网关,流量的出入口则不统一,客户端就需要知道所有服务的访问信息,微服务的意义将不复存在,所以,微服务网关在微服务系统架构中的存在是必要的。此外,API 网关在系统的可观测性、身份鉴权认证、稳定性和服务发现等方面也会发挥重要作用。</p> <h3>微服务遇到的挑战</h3> <p>微服务网关应该首先要具备 API 路由能力,微服务数量变多,API 数量急剧增加,网关还可以根据具体的场景作为流量过滤器来使用,以提供某些额外可选功能,因此对微服务 API Gateway 提出了更高要求,比如:</p> <ul> <li>可观测性:在以往的单体应用中,排查问题往往通过查看日志定位错误信息和异常堆栈;但是在微服务架构中服务繁多,出现问题时的问题定位变得非常困难;因此,如何监控微服务的运行状况、当出现异常时能快速给出报警,这给开发人员带来很大挑战。</li> <li>鉴权认证:而微服务架构下,一个应用会被拆分成若干个微应用,每个微应用都需要对访问进行鉴权,每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器,还包括其它服务的调用时,单体应用架构下的鉴权方式就不是特别合适了。在微服务架构下,要考虑外部应用接入的场景、用户 - 服务的鉴权、服务 - 服务的鉴权等多种鉴权场景。</li> <li>系统稳定性:若大量请求超过微服务的处理能力时,可能会将服务打垮,甚至产生雪崩效应、影响系统的整体稳定性。</li> <li>服务发现:微服务的分散管理,让微服务的负载均衡的实现也更具有挑战性。</li> </ul> <h3>解决方案</h3> <p>API 网关作为客户端和服务端的中间桥梁,为微服务系统提供统一的管理机制:除了基础的请求分发、API 管理和条件路由等功能,还包括身份验证、监控报警、调用链追踪、负载均衡、限流隔离和熔断降级。 身份认证:下图表示的是微服务联合 API 网关如何进行身份认证的,由图可见所有请求都通过网关,从而有效地隐藏了微服务。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/30/rlo8Ci1h_%E5%9B%BE2.png" alt="Architecture Diagram" referrerpolicy="no-referrer"></p> <p>监控报警/调用链追踪:API 作为客户端和服务端的中间桥梁,是微服务监控的最好载体,API 网关监控功能的主要职责是及时发现网关以及后端服务器的连接异常,在 API 的监控平台上面用户可以随时查看日志信息,监控信息,调用链等等,并且主机发生的任何异常都会自动报警到控制台。有些网关甚至可以做到给客户端和服务端双向报警。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/30/Zl9ydaQv_%E5%9B%BE3.png" alt="Architecture Diagram" referrerpolicy="no-referrer"></p> <p>限流隔离/熔断降级:随着互联网业务规模的增加,系统的并发度增高,多个服务之间相互调用链路,一条核心链路往往可能调用十个服务。如果在链路中,某个服务的 rt(响应时间)急剧上升,上游服务不断请求,造成恶性循环,上游等待结果线程数越多,使得更上游服务阻塞最终整条链路无法使用,从而导致服务雪崩,所以对入口流量进行整治管理是很有必要的,下图表示微服务系统是如何结合 API 网关进行限流隔离和熔断降级的。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/30/vEAHDq7c_%E5%9B%BE4.png" alt="Architecture Diagram" referrerpolicy="no-referrer"></p> <h3>主流网关选择</h3> <p>在微服务领域,有许多开源网关实现,有 NGINX、Kong、Apache APISIX 和 Envoy 等,Java 技术栈的有 Netfilx Zuul、Spring Cloud Gateway、Soul 等。或许你会问“有了 NGINX 和 Kong,为什么还需要 Apache APISIX ?” ,下面做个简单对比。</p> <table> <thead> <tr> <th>网关</th> <th>痛点</th> <th>优势</th> </tr> </thead> <tbody> <tr> <td>NGINX</td> <td>1. 修改配置需要 Reload 才能生效,跟不上云原生的发展。</td> <td>1. 老牌应用;<br> 2. 稳定可靠,久经考验;<br> 3. 高性能。</td> </tr> <tr> <td>Apache APISIX</td> <td>1. 文档不够丰富和清晰,需要待改进。</td> <td>1. Apache 基金会顶级项目;<br> 2. 技术架构更贴合云原生; 3. 性能表现优秀;<br> 4. 生态丰富; 5. 除了支持 Lua 开发插件外,还支持 Java、Go、Python、Node 等语言插件。</td> </tr> <tr> <td>Kong</td> <td>1. 默认使用 PostgreSQL 或 Cassandra 数据库,使得整个架构非常臃肿,并且会带来高可用的问题;<br> 2. 路由使用的是遍历查找,当网关内有超过上千个路由时,它的性能就会出现比较急剧的下降;<br> 3. 一些重要功能是需要付费的。</td> <td>1. 开源 API 网关的鼻祖,用户数众多;<br> 2. 性能满足大部分用户的需求;<br> 3. 生态丰富;<br> 4. 支持 Lua 和 Go 开发插件。</td> </tr> <tr> <td>Envoy</td> <td>1. 使用 C++,二次开发难度大;<br> 2. 除了 C++ 开发 filter 外,还支持 WASM 和 Lua。</td> <td>1. CNCF 毕业项目 更适合服务网格场景多语言架构部署。</td> </tr> <tr> <td>Spring Cloud Gateway</td> <td>1. 虽然 Spring 社区成熟,但是 Gateway 资源缺乏。</td> <td>1. 内置了非常多的开箱即用功能,并且都可以通过 SpringBoot 配置或者手工编码链式调用来使用; <br> 2. Spring 系列可扩展性强,易配置,可维护性好; <br> 3. Spring 社区成熟; <br> 4. 简单易用;<br> 5. 对于 Java 技术栈来说方便。</td> </tr> </tbody> </table> <h2>总结</h2> <p>随着互联网的发展,互联网企业的业务也在不断的飞速发展,进而导致系统的架构也在不断的发生着变化,微服务架构已经在众多公司得到广泛应用。随着微服务的数据越来越多,API 的数量也越来越多,对于大流量的治理,选择一个优秀的 API 网关是至关重要的。本文列举了常见网关,并进行对比,列出各自的优缺点,如果你正在做 API 网关的技术选型,或者你的微服务系统出现了性能问题,再或者你想搭建一个高效稳定的微服务系统,希望本文可以带给你一定的启发。</p>

为什么 APISIX Ingress 是比 Emissary-ingress 更好的选择?

<h2>背景</h2> <p>Kubernetes Ingress 是一种 API 对象,用于定义集群外部流量如何路由到集群内部服务的规则。Ingress Controller 通常用于实现 Ingress 资源的相关逻辑,并统一管理这些流量规则。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/20/DbvFS0JW_eb480415-2aaf-40b5-bfbd-98649c0d466a.jpeg" alt="Ingress" referrerpolicy="no-referrer"></p> <p>在实践中,企业用户往往需要 mTLS、重试、限流和鉴权等流量管理功能,但 Ingress 资源语义无法满足需要。因此,Ingress Controller 实现往往使用新增 CRD 等方式对功能进行扩展。接下来将详细介绍和对比 APISIX Ingresss 和 Emissary-ingress 两种实现的差异。</p> <h2>什么是 APISIX Ingress</h2> <p>Apache APISIX Ingress 是 Apache 软件基金会旗下的开源项目,其控制平面负责对 Kubernetes 中资源进行配置转换并进行交付,实际的业务流量则由 APISIX 承载。为了提高安全性,整个部署过程采用了数据面和控制面完全分离的架构,从而有效避免了数据面被攻击导致 Kubernetes 集群权限泄露的风险。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/20/9cUnKFRv_emi%202.jpeg" alt="apisix-ingress-controller" referrerpolicy="no-referrer"></p> <h2>什么是 Emissary-ingress</h2> <p>Emissary-ingress 是 CNCF 的孵化项目,作为 Envoy proxy 的控制平面,它负责解析 Kubernetes 资源,所有流量都直接由数据面 Envoy 来处理。通过将控制面和数据面打包为一个容器,使整体更易接入和部署。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/20/88AiEuWv_emi%203.jpg" alt="emissary-ingress" referrerpolicy="no-referrer"></p> <h2>APISIX Ingress 和 Emissary-ingress 的区别</h2> <p>除了对 Ingress 资源的支持外,两者都支持了 CRDs、Gateway API 的配置方式,以弥补 Ingress 语义的不足。</p> <p>下文将从基本能力、服务发现、可扩展性等方面分析两者之间的区别和优势。</p> <h3>基本功能</h3> <table> <tbody><tr> <td colspan="2" style="text-align:center">Feature</td> <td>APISIX Ingress</td> <td>Emissary-ingress</td> </tr> <tr> <td rowspan="5">Protocols</td> <td>HTTP/HTTPS</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>gRPC</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>TCP</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>UDP</td> <td>✓</td> <td>✕</td> </tr> <tr> <td>Websockets</td> <td>✓</td> <td>✓</td> </tr> <tr> <td rowspan="4">Load balance</td> <td>Round Robin</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>Ring Hash</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>Least Connections</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>Maglev</td> <td>✕</td> <td>✓</td> </tr> <tr> <td rowspan="5">Authentication</td> <td>External Auth</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>Basic</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>JWT</td> <td>✓</td> <td>✕</td> </tr> <tr> <td>OAuth</td> <td>✓</td> <td>✕</td> </tr> <tr> <td>OpenID</td> <td>✓</td> <td>✕</td> </tr> <tr> <td rowspan="5">Traffic Management</td> <td>Circuit Breaker</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>Rate Limiting</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>Canary</td> <td>✓</td> <td>✓</td> </tr> <tr> <td>Fault Injection</td> <td>✓</td> <td>✕</td> </tr> <tr> <td>Health Checks</td> <td>✓</td> <td>✓</td> </tr> </tbody></table> <p>常见的网关功能,包括流量管理、负载均衡、鉴权等。值得注意的是,Emissary-ingress 在鉴权方面的支持相对较少,只包含了最基本的 Basic Auth 和 External Auth 能力。</p> <h3>服务发现</h3> <p>在微服务架构中,应用通常被拆分为多个微服务,它们相互协作以完成具体的业务逻辑。由于微服务实例的数量不断变化,这就需要一种机制来帮助服务之间相互发现和定位。</p> <p>服务发现是指微服务在网络上的定位方式,通过服务名获取服务发现的信息以确定请求路由到哪个实例。对于传统的微服务框架,注册中心的选型往往是结合业务自身需求,如果将已存在的服务注册和发现组件迁移到基于 Kubernetes 的 DNS 服务发现机制,这需要一定的改造成本。如果网关支持现有的服务注册和发现组件,就不需要进行这些改造,从而更好地支持微服务框架。</p> <p>以下是两者对服务发现组件的支持情况:</p> <table> <thead> <tr> <th style="text-align:left">Service Discovery</th> <th style="text-align:left">Apache APISIX Ingress</th> <th style="text-align:left">Emissary-ingress</th> </tr> </thead> <tbody> <tr> <td style="text-align:left">Kubernetes</td> <td style="text-align:left">✓</td> <td style="text-align:left">✓</td> </tr> <tr> <td style="text-align:left">DNS</td> <td style="text-align:left">✓</td> <td style="text-align:left">✓</td> </tr> <tr> <td style="text-align:left">Nacos</td> <td style="text-align:left">✓</td> <td style="text-align:left">✕</td> </tr> <tr> <td style="text-align:left">Eureka</td> <td style="text-align:left">✓</td> <td style="text-align:left">✕</td> </tr> <tr> <td style="text-align:left">Consul</td> <td style="text-align:left">✓</td> <td style="text-align:left">✓</td> </tr> </tbody> </table> <p>在服务发现生态方面,APISIX Ingress 拥有着更高支持力度,用户可以非常方便的通过 Ingress Controller 集成到用户现有的微服务框架中。</p> <h3>可扩展性</h3> <p>当 Kubernetes Ingress Controller 的功能无法满足特定的需求时,用户可以通过二次开发的方式来扩展其功能。通过开发自定义插件或者修改现有的代码,可以满足更加个性化的需求。扩展性强的 Ingress Controller 可以更加方便地开发和定制化功能,为特定场景提供更好的支持和解决方案。</p> <h4>Emissary-ingress</h4> <p>Emissary-ingress 可扩展性是比较差的,不支持通过自定义 Envoy Filter 的方式进行拓展。由于数据面和控制面作为一个整体,这就需要对整体进行二次开发,数据面 Envoy 的二开复杂度高,这给开发者带来了很大的负担。</p> <p>除此之外,如果用户需要更强大的 Emissary-ingress,需要将其升级为 Ambassador 的商业产品 <a href="https://www.getambassador.io/products/edge-stack/api-gateway">Edge Stack</a>,一些专有功能需要付费以<a href="https://www.getambassador.io/docs/edge-stack/latest/about/faq#can-i-use-the-add-on-features-for-aesambassador-edge-stack-for-free">获取支持</a>。</p> <h4>APISIX Ingress</h4> <p>数据面 APISIX 主要通过插件的方式进行功能扩展,提供了 80+ 开箱即用的插件。APISIX Ingress 支持了 APISIX 提供的所有插件,可满足大多数日常使用场景。</p> <p>如果需要根据自身的业务场景进行功能定制,APISIX 提供了多种扩展方式,可以根据自身情况自由选择、组合。 目前支持的扩展方式如下:</p> <ol> <li>使用 Lua 语言进行插件开发,这种方式相对简单,并且几乎没有性能损耗。此外还可以通过 serverless 插件来直接编写 Lua 代码,快速的满足业务需求。</li> <li>除了内置原生的 Lua 语言,还可以通过 Plugin Runner 或 WASM 插件来进行扩展,这种模式下支持 Java/Python/Go 等语言开发自定义插件。使用户能够利用一些现有的业务逻辑,还可以根据公司已有技术栈或研发喜好自行选择,而无需学习新语言。</li> </ol> <p>以上扩展方式,APISIX Ingress 都能够完整的支持,无需进行额外的开发。</p> <h3>性能</h3> <p>作为 Kubernetes 入口流量代理组件,接管了平台所有的入口流量,统一管理多种流量规则,这对代理的性能也有了更高的要求。</p> <p>本文将在相同的实例(4C 8G)中,分别对 APISIX Ingress(APISIX:3.1.0) 和 Emissary-ingress 3.4.0 进行性能测试。</p> <h4>QPS</h4> <p>QPS(Queries-per-second),每秒查询率:服务每秒处理的请求数量,数值越大性能越好。</p> <ul> <li>单个 Ingress 资源 QPS</li> </ul> <p><img src="https://static.apiseven.com/uploads/2023/02/20/9lwgYkYK_emi%204.jpeg" alt="1-ingress-qps" referrerpolicy="no-referrer"></p> <ul> <li>5000 Ingress 资源 QPS</li> </ul> <p><img src="https://static.apiseven.com/uploads/2023/02/20/USHuHlsr_emi%205.jpg" alt="5000-ingress-qps" referrerpolicy="no-referrer"></p> <h4>Latency</h4> <p>响应延迟:服务器响应时间,延迟越小,性能越好</p> <p><img src="https://static.apiseven.com/uploads/2023/02/20/5H1yD69n_emi%206.jpg" alt="latency" referrerpolicy="no-referrer"></p> <h4>小结</h4> <p>从图中可以发现,在面对不同的资源规模时,APISIX Ingress 的性能不会受到影响,表现十分均衡。而 Emissary-ingress 在资源规模较大时,匹配不同的路由对 QPS 和延时产生了严重的影响,其性能随着资源数量的增加而不断下降。由此可见,在实际生产环境中,随着业务体量的不断增长,APISIX 的高性能优势更加凸显。</p> <h2>总结</h2> <p>Emissary-ingress 的特点在于使用简单易于接入,但是二次开发的难度较高。对于更多的功能需求,需要通过接入平台的相关组件来获取支持。</p> <p>相比之下,APISIX Ingress 在可扩展性和服务发现集成方面具有优势,扩展性强且开发简单。此外,APISIX Ingress 的性能表现出色,特别是在业务规模不断增长的场景中具有明显优势。</p>

APISIX 是怎么保护用户的敏感数据不被泄露的?

<h2>什么是敏感数据</h2> <p>敏感数据,又称隐私数据,主要是指泄露后可能会给个人或者公司带来严重危害的数据,包括但不限于个人身份信息,企业经营数据等。</p> <h2>为什么要保护敏感数据</h2> <p>对于个人来说,如果敏感数据被泄露,轻则可能受到无止尽的广告骚扰,重则可能导致人格尊严受到侵害,或者人身、财产安全受到危害。</p> <p>对于企业来说,敏感数据直接关系企业的信息安全。例如密钥,证书等敏感信息,一旦遭受泄露,企业将会遭受信任损失和财务损失,甚至可能面临法律责任。</p> <h2>在 API 网关中有哪些敏感数据</h2> <p>API 网关作为业务流量的入口,往往包含了大量的敏感数据,例如 API 密钥、用于鉴权认证的 token 等,除了基本的负载均衡、流量管理等功能外,提升安全、防止敏感信息泄露,也至关重要,因此对 API 网关中的敏感数据进行保护具有重要意义。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/03/a86d0nVj_sensitive_data.png" alt="Sensitive Data" referrerpolicy="no-referrer"></p> <h2>API 网关怎么保护隐私数据</h2> <p>大致思路一般如下:</p> <ol> <li> <p>将敏感数据置于保护区内,对访问权限进行严格的控制</p> </li> <li> <p>增加风控系统,对异常行为和业务合规进行风险控制</p> </li> <li> <p>对敏感数据进行脱敏处理或加密存储</p> </li> </ol> <p>下面将以 Apache APISIX 为例,展示如何在 API 网关中对敏感数据进行保护。</p> <h2>Apache APISIX 在保护隐私数据中的实践</h2> <p><a href="https://apisix.apache.org/">Apache APISIX</a> 是 Apache 软件基金会的顶级开源项目,也是当前最活跃的开源网关项目。作为一个动态、实时、高性能的开源 API 网关,Apache APISIX 提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。</p> <p>用户可以通过编写插件来对 Apache APISIX 功能进行扩展,得益于成熟的社区和越来越多的活跃开发者,Apache APISIX 的插件数量在日益增多,其中有些插件携带敏感信息,例如 <a href="https://apisix.apache.org/docs/apisix/plugins/jwt-auth/">jwt-auth</a> 插件的配置项 <code>secret</code> 和 <code>private_key</code>,为了防止这些数据被非法获取,我们有必要对其进行加密存储。</p> <p>为了增强 APISIX 的安全性,更好的保护用户的隐私,APISIX 在 3.1.0 版本中引入了 <code>Global Data Encryption</code> 功能,有了这个功能,开发者在开发新插件的时候,只需要在插件的 schema 中指定要加密的数据,APISIX 就能在控制面写入的时候自动进行加密存储,数据面读取的时候自动解密,对开发者完全透明。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/03/o7B7uR2a_global_data_encryption.png" alt="Global Data Encryption" referrerpolicy="no-referrer"></p> <p>下面我们来看看具体的示例。</p> <h3>未开启数据加密功能</h3> <ol> <li>下发配置</li> </ol> <pre><code class="language-lua">curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "foo", "plugins": { "basic-auth": { "username": "foo", "password": "bar" } } }' </code></pre> <ol start="2"> <li>确定 etcd 中的敏感数据状态</li> </ol> <pre><code class="language-lua">etcdctl get /apisix/consumers/foo {"username":"foo","update_time":1675414313,"create_time":1674009211,"plugins":{"basic-auth":{"username":"foo","password":"bar"}}} </code></pre> <p><strong>可以看到 <code>password</code> 字段是明文存储</strong></p> <h3>开启数据加密功能</h3> <ol> <li>在 <code>config.yaml</code> 中开启 <code>data_encryption</code>:</li> </ol> <pre><code class="language-yaml">apisix: data_encryption: enable: true keyring: - edd1c9f0985e76a2 </code></pre> <ol start="2"> <li>启用已经支持敏感数据加密的插件,这里以 <code>basic-auth</code> 插件为例</li> </ol> <pre><code class="language-lua">curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "foo", "plugins": { "basic-auth": { "username": "foo", "password": "bar" } } }' </code></pre> <ol start="3"> <li>验证插件功能</li> </ol> <pre><code>curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "methods": ["GET"], "uri": "/get", "plugins": { "basic-auth": {} }, "upstream": { "type": "roundrobin", "nodes": { "httpbin.org": 1 } } }' </code></pre> <p>能正常访问</p> <pre><code>curl -i -ufoo:bar http://127.0.0.1:9080/get HTTP/1.1 200 OK ... </code></pre> <p>密码错误,不能访问</p> <pre><code>curl -i -ufoo:test http://127.0.0.1:9080/get HTTP/1.1 401 Unauthorized ... {"message":"Invalid user authorization"} </code></pre> <p>插件功能正常</p> <ol start="4"> <li>确定 etcd 中的敏感数据状态</li> </ol> <pre><code class="language-lua">etcdctl get /apisix/consumers/foo {"create_time":1674009211,"update_time":1674009211,"plugins":{"basic-auth":{"password":"+kOEVUuRc5rC5ZwvvAMLwg==","username":"foo"}},"username":"foo"} </code></pre> <p><strong>可以看到 <code>password</code> 字段已经被成功加密</strong>,此时就算数据被泄露,其他人也无法破解。</p> <h2>总结</h2> <p>在 API 网关中,包含了大量的敏感数据,因此需要采取有效的手段来对数据进行保护,本文以 APISIX 作为例子,为大家介绍了如何借助 <code>Global Data Encryption</code> 功能来保护敏感数据,确保不会有任何敏感数据进行明文存储,这样即使 etcd 中所有存储的数据都被盗取,也不会造成敏感数据泄露,从而有效提升了 APISIX 的安全性。同时除了对敏感数据进行加密以外,APISIX 还支持将敏感信息直接放到第三方服务,即<a href="https://api7.ai/blog/how-to-use-secret-manager-with-api-gateway">Secret Manager</a>功能,进一步提升了安全性。</p> <p>希望通过上述分享,能够使大家更多的了解如何在 API 网关中保护敏感数据,从而保障企业的信息安全。</p>

API 网关日志的价值,你了解多少?

<h2>网关日志的价值</h2> <p>在数字化时代,软件架构随着业务成长而变得越来越复杂,这给故障的发现和排查都带来了非常大的挑战,进一步倒逼软件重视自身的可观测性能力。 而<strong>日志被称为可观测性的三大支柱之一</strong>,它可以帮助系统管理员和开发人员了解系统的运行状态,以便能够及时发现并解决问题。 此外,日志还可以用于数据挖掘、审计和安全监控等方向,以确保系统的合规性和安全性。</p> <p>而 API 网关是在应用程序与外部世界之间构建的一道关口,它可以帮助组织更好地管理和监控 API 调用,其中之一重要的功能就是对于 API 调用日志的记录。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/29/q5vt6NM5_api-gateway-log.png" alt="API Gateway Log" referrerpolicy="no-referrer"></p> <p>下面将会从两个角度来分析 API 网关日志的价值。</p> <h3>运维价值</h3> <p>无论是传统系统运维,还是现代网站可靠性工程师(SRE),对于保障系统稳定性这个大目标而言,如何发现故障永远是排在最高优先级的任务,因为对于任何一个大型的在线业务,每一秒的故障时间都可能会带来巨大的业务损失和用户体验伤害。</p> <p>而 API 网关处于整个系统的最前端,它可以扮演一个“哨兵”的角色,让我们从它的访问日志中提取非常多对于运维体系至关重要的指标数据:</p> <ul> <li>通过上、下游状态码分析,监控网站整体可用性和具体上游服务的可用性;</li> <li>通过访问日志整体统计,监控网站流量,及时发现 DDoS 一类的攻击;</li> <li>流量的趋势分析可以作为业务系统扩缩容的参考依据;</li> <li>请求耗时统计,为业务系统提供接口级别的性能报告,作为业务系统性能优化的数据参考。</li> </ul> <p>以上都是一些在业界被广泛采用的 API 网关日志的最佳实践,在很多企业的运维体系中落地。</p> <h3>业务价值</h3> <p>与运维价值被广泛认可相比,网关日志的业务价值往往是被人们所忽视的。 例如在用户行为分析领域,在程序中使用编码的方式进行埋点上报是最常见的数据收集方式,而对于一套设计良好的 API 来说,网关日志是可以天然支持此类需求的。我们可以:</p> <ul> <li>通过分析客户端 IP,统计流量的地理位置分布;</li> <li>通过分析 HTTP Referer,了解用户进入各个页面的来源;</li> <li>对于重点 API 的聚合统计,可以直接得到关键业务指标。例:统计用户注册、商品下单等 API 的成功调用,可以直接得到指定时间段内的新用户、新订单的数量指标。</li> </ul> <p>当然网关日志显然不如程序埋点灵活,无法实现自定义数据收集需求,但其中蕴藏的业务价值足以满足简单的数据挖掘需求。</p> <p>Apache APISIX 是一个动态、实时、高性能的云原生 API 网关,提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。 它的很多能力是通过可插拔的插件形式提供,这其中就包括十数个日志插件。下面将会以 APISIX 中典型的日志插件为例,介绍如何将网关日志与日志分析系统打通,挖掘出更多的价值。</p> <h2>APISIX 典型日志插件介绍</h2> <h3>elasticsearch-logger</h3> <p><img src="https://static.apiseven.com/uploads/2023/01/29/0qU8RQOO_elastic-apisix.png" alt="Elastic Search And Apache APISIX" referrerpolicy="no-referrer"></p> <p>ElasticSearch 是一个分布式开源搜索和分析引擎,旨在处理大量数据,在日志分析领域享有极高知名度。 它提供的配套数据面板 Kibana,可以非常轻松的定制出各种统计图表,满足组织对于可视化查询分析的需求。</p> <p>实际应用时,由于大多数传统软件的日志都是保存到本地文件,所以 ElasticSearch 生态中有一个项目叫 Filebeat,用于监控本地机器上的日志文件,并将增量的日志发送给 ElasticSearch 服务器。而 APISIX 提供的 <code>elasticsearch-logger</code> 插件,可以将 APISIX 的访问日志直接发送 ElasticSearch 服务器,这有几点好处:</p> <ul> <li>无需部署 Filebeat 组件,处理链路更短,减少计算资源消耗;</li> <li>日志不落盘,不用考虑日志文件的磁盘占用,因为访问日志的量级可能非常大,如果没有处理好文件的轮转,很容易将机器的磁盘打满,导致故障。此外,与磁盘进行交互也会一定程度上降低网关的性能。</li> </ul> <h3>kafka-logger</h3> <p>网关访问日志有一个很明显的特点:日志的数据量与业务的请求量成正比,请求越多,日志越多。对于大多数在线业务,请求量会存在明显的周期性,比如外卖平台的流量高峰期会出现在餐点附近,而视频网站的高峰期则会出现在下班时间段。 这对于日志存储系统的挑战是巨大的,如何保证系统在流量高峰期依然可以正常工作,是每一个 ElasticSearch 管理员的必修课。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/29/aNv4oh2Q_apisix-kafka-es.png" alt="Apache APISIX And Kafka" referrerpolicy="no-referrer"></p> <p>消息队列是处理“削峰填谷”场景的不二法器,在网关与存储系统之间引入消息队列,提供日志缓冲区,将会极大的降低存储系统在流量高峰期的压力。 为此 APISIX 提供了 <code>kafka-logger</code> ,它可以将访问日志投递到 Kafka 服务器,避免对日志存储产生直接的冲击。</p> <h3>loggly</h3> <p>近几年 SaaS 的概念逐渐深入人心,它凭借极低的接入门槛和按需付费的计价模型被众多中小型企业所青睐,在日志分析领域也涌现了诸多 SaaS 产品,Loggly 凭借着丰富的日志来源和分析手段成为其中的佼佼者。 在此背景下,活跃的 APISIX 社区开发了开箱即用的 <code>loggly</code> 插件,只需要配置好凭证信息,就可以将 APISIX 访问日志直接发送给 Loggly 服务,十分方便。</p> <p>类似 <code>loggly</code> 插件,APISIX 还提供了 <code>google-cloud-logging</code> 、<code>sls-logger</code> 、<code>tencent-cloud-cls</code> 插件,可以轻松集成主流云厂商的日志服务。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/29/LnOhd04T_apisix-log-saas.png" alt="Apache APISIX Log SaaS" referrerpolicy="no-referrer"></p> <h3>error-log-logger</h3> <p>上面介绍的插件都是用于收集访问日志,在 APISIX 中还存在另外一类日志:错误日志(error.log),它对于排查网关自身故障非常关键,所以 APISIX 提供了 <code>error-log-logger</code> 插件 ,用于将错误日志发送到远端服务器进行存储分析。实际使用时,可以结合 APISIX 的日志等级配置,让网关打印出更多 debug 或者 info 级别的日志,这两个级别会包含更加详细的网关运行状态日志,利用这些日志通常可以定位出绝大多数问题。</p> <h2>总结</h2> <p>网关日志蕴藏着巨大的价值,从 Apache APISIX 项目丰富的日志插件中我们也可以看出,社区中的企业用户对于网关日志价值的肯定,而这些插件也进一步降低了新用户搭建日志系统的成本。 此外,APISIX 还有另外两类可观测性插件:<code>metrics</code>、<code>tracing</code>,它们配合上日志插件将会进一步提升网关的可观测性,助力系统稳定性建设。</p>

如何使用 Kubernetes 实现应用程序的弹性伸缩

<h2>介绍</h2> <p>通常情况下,每个应用可以承载的压力都是固定的,我们可以通过提前进行压测来了解单应用程序副本的负载能力。如果在业务高峰,或者业务的请求压力增加时候,对应用进行横向扩容可以保证更好的为用户提供服务。</p> <p>Apache APISIX 是一个高性能的云原生 API 网关,所有发送到上游应用程序的流量都将通过 APISIX,所以我们可以根据 APISIX 提供的流量指标,来判断应用程序是否需要进行弹性伸缩。</p> <p>本文中将使用 KEDA 作为弹性伸缩的控制组件,用 Prometheus 采集 APISIX 提供的流量指标来进行应用的弹性伸缩。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/21/Zrw8DGl2_0221.png" alt="using KEDA for autocaling" referrerpolicy="no-referrer"></p> <h2>KEDA 中如何使用 Prometheus 实现伸缩</h2> <p><a href="https://keda.sh/">KEDA</a> 是一个 Kubernetes 中基于事件的自动伸缩组件,可以配置多种伸缩器。本文将使用 Prometheus 作为伸缩器 ,获取 APISIX 暴露出来的 metrics(指标)并进行应用程序的扩缩容。</p> <h3>部署 KEDA</h3> <p>KEDA 的部署比较简单,添加对应的 Helm repo 并进行安装即可。</p> <pre><code>(MoeLove) ➜ helm repo add kedacore https://kedacore.github.io/charts "kedacore" has been added to your repositories (MoeLove) ➜ helm repo update kedacore Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "kedacore" chart repository Update Complete. ⎈Happy Helming!⎈ (MoeLove) ➜ helm install keda kedacore/keda --namespace keda --create-namespace NAME: keda LAST DEPLOYED: Thu Jan 19 00:01:00 2023 NAMESPACE: keda STATUS: deployed REVISION: 1 TEST SUITE: None </code></pre> <p>在安装完成后,Pod 处于 <code>Running</code> 状态,表示已经正常安装。</p> <pre><code>(MoeLove) ➜ kubectl -n keda get pods NAME READY STATUS RESTARTS AGE keda-operator-metrics-apiserver-6d4db7dcff-ck9qg 1/1 Running 0 36s keda-operator-5dd4748dcd-k8jjz 1/1 Running 0 36s </code></pre> <p>接下来部署 Prometheus。</p> <h3>部署 Prometheus</h3> <p>此处我们使用 Prometheus Operator 来进行 Prometheus 的部署。Prometheus Operator 可以帮助我们在 Kubernetes 中快速部署 Prometheus 实例,以及通过声明式配置的方式添加监控规则。</p> <p>通过如下步骤完成 Prometheus Operator 的安装。</p> <pre><code>(MoeLove) ➜ https://github.com/prometheus-operator/prometheus-operator/releases/download/v0.62.0/bundle.yaml (MoeLove) ➜ kubectl apply --server-side -f bundle.yaml customresourcedefinition.apiextensions.k8s.io/alertmanagerconfigs.monitoring.coreos.com serverside-applied customresourcedefinition.apiextensions.k8s.io/alertmanagers.monitoring.coreos.com serverside-applied customresourcedefinition.apiextensions.k8s.io/podmonitors.monitoring.coreos.com serverside-applied customresourcedefinition.apiextensions.k8s.io/probes.monitoring.coreos.com serverside-applied customresourcedefinition.apiextensions.k8s.io/prometheuses.monitoring.coreos.com serverside-applied customresourcedefinition.apiextensions.k8s.io/prometheusrules.monitoring.coreos.com serverside-applied customresourcedefinition.apiextensions.k8s.io/servicemonitors.monitoring.coreos.com serverside-applied customresourcedefinition.apiextensions.k8s.io/thanosrulers.monitoring.coreos.com serverside-applied clusterrolebinding.rbac.authorization.k8s.io/prometheus-operator serverside-applied clusterrole.rbac.authorization.k8s.io/prometheus-operator serverside-applied deployment.apps/prometheus-operator serverside-applied serviceaccount/prometheus-operator serverside-applied service/prometheus-operator serverside-applied </code></pre> <p>然后使用如下配置作为 Prometheus 实例的配置,然后将其应用到 Kubernetes 集群中。</p> <pre><code class="language-yaml">--- apiVersion: v1 kind: ServiceAccount metadata: name: prometheus --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: prometheus rules: - apiGroups: [""] resources: - nodes - nodes/metrics - services - endpoints - pods verbs: ["get", "list", "watch"] - apiGroups: [""] resources: - configmaps verbs: ["get"] - apiGroups: - networking.k8s.io resources: - ingresses verbs: ["get", "list", "watch"] - nonResourceURLs: ["/metrics"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: prometheus roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: prometheus subjects: - kind: ServiceAccount name: prometheus namespace: default --- apiVersion: monitoring.coreos.com/v1 kind: Prometheus metadata: name: prometheus spec: serviceAccountName: prometheus serviceMonitorSelector: matchLabels: app: apisix serviceMonitorNamespaceSelector: matchLabels: team: apisix resources: requests: memory: 400Mi enableAdminAPI: false --- apiVersion: v1 kind: Service metadata: name: prometheus spec: type: LoadBalancer ports: - name: web port: 9090 protocol: TCP targetPort: web selector: prometheus: prometheus </code></pre> <p>应用后,则可以看到在 <code>default</code> namespace 下创建了 Prometheus 实例。由于上述配置中创建了 <code>LoadBalancer</code> 类型的 Service,所以可以直接通过 LoadBalancer 的公网 IP 进行 Prometheus 的访问。</p> <pre><code class="language-bash">(MoeLove) ➜ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.43.0.1 &lt;none&gt; 443/TCP 96m prometheus-operator ClusterIP None &lt;none&gt; 8080/TCP 92m prometheus-operated ClusterIP None &lt;none&gt; 9090/TCP 41m prometheus LoadBalancer 10.43.125.194 216.6.66.66 9090:30099/TCP 41m </code></pre> <h2>如何部署网关并开启监控</h2> <p>接下来部署 APISIX Ingress,并使用 Prometheus 进行 metrics 采集。</p> <p><strong>如果用户没有使用 APISIX Ingress,而是仅仅使用了 APISIX,操作方法也是类似的。</strong> 这里不再分开介绍。</p> <p>此处使用 Helm 进行部署,可以同时将 APISIX Ingress controller 和 APISIX 部署到集群中。</p> <pre><code>(MoeLove) ➜ helm repo add apisix https://charts.apiseven.com "apisix" already exists with the same configuration, skipping (MoeLove) ➜ helm repo update apisix Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "apisix" chart repository Update Complete. ⎈Happy Helming!⎈ (MoeLove) ➜ helm upgrade --install apisix apisix/apisix --create-namespace --namespace apisix --set gateway.type=LoadBalancer --set ingress-controller.enabled=true --set ingress-controller.config.apisix.serviceNamespace=apisix Release "apisix" has been upgraded. Happy Helming! NAME: apisix LAST DEPLOYED: Thu Jan 19 02:11:23 2023 NAMESPACE: apisix STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: 1. Get the application URL by running these commands: NOTE: It may take a few minutes for the LoadBalancer IP to be available. You can watch the status of by running 'kubectl get --namespace apisix svc -w apisix-gateway' export SERVICE_IP=$(kubectl get svc --namespace apisix apisix-gateway --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}") echo http://$SERVICE_IP:80 </code></pre> <p>接下来开启 APISIX 的 <code>prometheus</code> 插件,具体的配置方法和相关参数可以参考如下两篇文档。</p> <ul> <li><a href="https://apisix.apache.org/docs/apisix/plugins/prometheus/">prometheus plugins | Apache APISIX®</a></li> <li><a href="https://apisix.apache.org/docs/ingress-controller/tutorials/how-to-access-Apache-APISIX-Prometheus-Metrics-on-k8s/">How to access Apache APISIX Prometheus metrics on Kubernetes | Apache APISIX®</a></li> </ul> <p>开启后,便可以通过创建 ServiceMonitor 资源,让 Prometheus 抓取 APISIX 暴露出的 metrics 了。</p> <pre><code class="language-yaml">apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: example-app labels: app: apisix spec: selector: matchLabels: app: apisix endpoints: - port: web </code></pre> <h2>验证应用弹性伸缩能力</h2> <p>此处将创建一个示例应用。</p> <pre><code>(MoeLove) ➜ kubectl create deploy httpbin --image=kennethreitz/httpbin --port=80 deployment.apps/httpbin created (MoeLove) ➜ kubectl expose deploy httpbin --port 80 </code></pre> <p>创建如下路由规则,应用到 Kubernetes 集群后,则可通过 APISIX 进行请求的代理。</p> <pre><code class="language-yaml">apiVersion: apisix.apache.org/v2 kind: ApisixRoute metadata: name: httpserver-route spec: http: - name: rule1 match: hosts: - local.httpbin.org paths: - /* backends: - serviceName: httpbin servicePort: 80 </code></pre> <p>接下来,创建 KEDA 的 ScaledObject,配置 Prometheus 相关参数。</p> <pre><code class="language-yaml">apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: prometheus-scaledobject namespace: default spec: scaleTargetRef: name: httpbin triggers: - type: prometheus metadata: serverAddress: http://prometheus.default.svc:9090 metricName: apisix_http_status threshold: '10' query: sum(rate(apisix_http_status{route="httpserver-route"}[1m])) </code></pre> <p>上述参数表示通过 <code>sum(rate(apisix_http_status{route="httpserver-route"}[1m]))</code> 作为查询表达式,如果结果能到达 10, 则开始进行扩容(此处配置仅用于本文中的示例使用,生产环境请按照实际情况进行修改)。</p> <p>然后,我们通过 curl 向 httpbin 服务发出连续请求,再次查看示例应用的 Pod 已经变成两个,证明 KEDA 成功自动扩容了。</p> <pre><code>(MoeLove) ➜ kubectl get pods NAME READY STATUS RESTARTS AGE httpbin-d46d778d7-chtdw 1/1 Running 0 12m httpbin-d46d778d7-xanbj 1/1 Running 0 10s </code></pre> <p>待一段时间无请求后,再次查看发现 Pod 的数量自动缩减为一个,证明自动缩容也实现了。</p> <pre><code>(MoeLove) ➜ kubectl get pods NAME READY STATUS RESTARTS AGE httpbin-d46d778d7-chtdw 1/1 Running 0 32m </code></pre> <h2>总结</h2> <p>本篇文章利用 KEDA 使用 Prometheus 采集 APISIX 暴露出来的指标作为伸缩器,进而实现基于流量的应用程序弹性伸缩。由于所有流量都会先经过 APISIX ,所以在 APISIX 侧进行数据统计更加简单方便。</p> <p>在业务请求量上来后,应用程序将进行自动化的扩容,当业务低谷的时候,则会自动的缩容。这可以在缓解很多生产环境下的手动扩/缩容操作,以保障用户的服务体验。</p>

多云和混合云场景下的 API 管理:挑战与选择

<h2>一、多云和混合云</h2> <p>如今微服务已经成为最流行的一种软件架构,人们通过自己对业务的理解,和科学方法(比如领域驱动设计的理论)的加持将组织对外提供的产品拆分为一个个微服务,同时按照微服务的架构调整组织架构(逆康威定律)来开展业务的迭代。</p> <p>以往组织习惯于自建数据中心来部署自家的产品,这些数据中心可能构建在买断或者是租赁的机房中,组织需要对机房的设备进行复杂的管理,包括交换机、服务器、磁盘、机柜等这些硬件设施,以及应对因为机房掉电、机柜温度过高、服务器崩溃等带来的故障。应对这些要问题往往会花费大量的人力财力,同时达不到很好的效果,使得自己产品的 SLA(服务等级约定)往往无法达到对客户的承诺,从而造成赔偿。</p> <p>随着云的兴起,人们越来越习惯于将自己的业务部署到公有云之上,公有云帮助用户屏蔽了硬件的细节,工程师再也不用关心基础设施的搭建和维护问题,而是可以把自己的精力尽可能得放到业务的部署、维护和开发之上。然而,除了享受云带来的便利以外,云的兴起和使用也给用户带来了其他的一些问题:</p> <ul> <li><strong>被“锁定”</strong>,当深度使用了云上某产品后,导致业务无法轻易迁移到其他的云,或者是从云上离开。这种情况特别发生在使用云提供的 PaaS 或者 SaaS 产品,然后与自己的业务发生了强绑定。通常因为在选购产品时,业务正处于高速发展的阶段,决策人往往忽略了使用云产品所带来的捆绑效应。</li> <li><strong>可用性问题</strong>,大家都懂得“不要把鸡蛋放在一个篮子里”的道理,然后在业务上升期时,组织考虑的是快速上线产品,占据市场,对于基建的考虑往往是不足的,在这样的情况下,如果发生云某某区域机房掉电、光缆被挖断等问题,就会对业务造成影响。 于是现在人们越来越习惯于同时使用多种公有云或者除了使用云以外,额外再搭建私有云的方式,来规避上述提到的问题。这就是我们常说的“多云”和“混合云”。</li> </ul> <p>“多云”指的是同时使用多种公有云,将业务镜像地、或者异构地部署到这些云上,同时尽可能采用标准服务(避免厂商锁定)。因为使用了多种云,当某个云出现不可用的情况下,它对业务的影响能够被缩小,甚至通过 DNS 修改解析等方式,启用备份的云,从而确保业务的连续性。“混合云”指的是组织除了使用了一个或者多个公有云以外,还拥有自建的私有云(或者数据中心),在此场景下,部分业务可能部署在私有云,其他的可能都已经上公有云,或者所有业务都在云上,而私有云负责管理和监控。<strong>总之,在采用了“多云”或者“混合云”这样的模式后,在提升服务可用性的同时,软件部署方式的灵活性也大大提升。</strong></p> <p>然而,在应用“多云”和“混合云”后,如何高效、简单的管理云上的微服务,则变得更加棘手。这当中比较经典的要属 API 的管理了,如今大量的微服务都选择使用 API 这一方式来进行交互,当微服务部署之后,它们提供的 API 需要能够被暴露到外部,以便和外部的调用方连接,从而提供服务。</p> <h2>二、多云、混合云场景下 API 管理的需求</h2> <p>我们知道对于管理微服务 API 而言,一款好的 API 网关是必不可少的,API 网关能够安全、高效地将微服务 API 暴露出去,然而,采用“多云“,“混合云”这样的模式后,对于 API 网关的需求不仅仅局限在 API 网关自身的功能是否满足业务的需求了,具体来说,我们需要考虑:</p> <h3>是否支持多集群管理</h3> <p>正如前文所述,在采用“多云”或者“混合云”,后,每个云或者私有数据中心所部署的业务可能存在着比较大的差异,因此用户需要在这些云上分别部署一套套的 API 网关集群,且可想而知这些网关集群的配置、证书、API 密钥等可能不尽相同。如果用户选择使用的网关不支持多集群的管理,可能会对 API 的管理造成很大的麻烦,尤其是在业务扩张期,API 网关集群规模越来越大,数量越来越多的时候。</p> <p>在这种情况下,一款支持多集群管理的 API 网关产品 便能极大地降低管理员的管理成本。</p> <ol> <li>用户拥有一个统一的控制台,并且在该控制台中可选择当前需要配置和查看的集群,并且可以根据实际业务的部署情况,上线或者下线一个网关集群。</li> <li>用户可以控制台上查看所有这些 API 网关集群的运行状态,包括常见的 QPS、延迟、网关集群 CPU 和内存占用率等。</li> </ol> <h3>是否支持协作</h3> <p>由于业务的快速发展,少数几个 API 网关管理员可能无法承担起维护全部 API 网关集群的重担,同时考虑到大部分的网关配置,比如添加路由、为路由配置插件等,可以由开发者自行处理,不需要全部由管理员经手。因此,是否支持“协作”对于管理来说,也变得重要起来。具体来说,管理员可以邀请组织内的其他成员加入到该 API 网关集群的管理中,同时使用 RBAC 之类的策略为大家分配不同的权限,比如为管理员设置 Organization Admin 的角色(可以执行任意操作),为普通开发者设置为 Service Admin 的角色(仅可维护少数几个服务和路由),进而在实现协作的基础上确保操作安全,并且在员工离职或者出现岗位变动时,及时回收账号或者修改权限。</p> <p>试想如果不支持协作,那么大概率一个组织内的成员会共享一个账号,这种用法在初期可能具备一定的便利性,但是时间一长它的潜在危害就会暴露出来,比如某员工在离职后可能依然可以登录并进行配置,甚至一些不怀好意的员工可能会选择删除全部数据,这对于组织对外的产品和服务来说是灾难性的。而且在进行复盘时,管理员甚至无法将具体的操作和具体某个成员关联起来,因为大家共享同一账号(即便是保有审计日志的情况下)。</p> <h3>是否可运行在任何基础设施上</h3> <p>随着容器化和容器编排的技术越来越成熟,以往很多运行在虚拟机之上的微服务都纷纷转投了 Kubernetes 的怀抱,这就意味着,用户可能会选择使用 Kubernetes、传统的虚拟机、甚至是物理机(比如在自己的私有数据中心里)。如果用户选择的 API 网关产品在功能上非常丰富,能够满足业务需求,但是又受限于底层基础设施,或者缺乏成熟的安装工具,这样会让用户处于两难的境地,要么放弃使用,要么自行进行二次开发从而使得该 API 网关获得运行在某个基础设施之上的能力。无论如何,这都会带来使用上的成本。</p> <h2>三、选择</h2> <p>结合上文所述,在“多云”和“混合云”的场景下,如何选择 API 网关则变得极其重要,这里列举了几种常见的选择,用户应该根据他们的实际场景和发展的考虑来进行分析。</p> <h3>不同云不同方案</h3> <p>通常来说每一家公有云厂商都会提供其内置的 API 网关解决方案,用户可以根据现有产品的开发规格对云上的 API 网关方案进行选型。</p> <p>优点如下:</p> <ol> <li>使用成本极低,无需部署,无需维护</li> <li>通常支持按需付费,早期使用可能只需要极低的费用</li> </ol> <p>缺点是:</p> <ol> <li>往往不同云的 API 网关使用方式相互不兼容,完成同一种配置需要经历不同的路径</li> <li>各家的产品功能不对称,比如 A 厂商可能支持 mTLS,而 B 厂商不支持</li> <li>“混合云”场景下,可能需要再选择一款开源的或者商业化的 API 网关,部署在用户自己的私有云上</li> </ol> <p>采用该方案时,如果想要获得一致的使用体验,可能需要基于不同的解决方案进行产品化,开发一个配置同步产品,屏蔽底层的细节,这里可能需要用户自行进行调研,分析以及开发(但可能会遇到兼容性问题,功能受限,只能使用几个 API 网关产品的交集功能),且需要考虑同步失败情况。</p> <p><img src="https://static.apiseven.com/2022/12/26/63a9556eb5142.png" alt="Configuration Syncer" referrerpolicy="no-referrer"></p> <p>当然,用户也可以选择手动重复配置每一个云上的 API 网关(如果可以忍受的话)。</p> <p><img src="https://static.apiseven.com/2022/12/26/63a9556f69bb6.png" alt="Duplicated Configuration" referrerpolicy="no-referrer"></p> <h3>使用开源解决方案</h3> <p>考虑到在不同云使用不同方案的缺点,用户也可以考虑使用开源的 API 网关解决方案(如 Apache APISIX, Kong, Tyk, Traefik 等),这样的好处是,你可以在每个云(包括私有数据中心)上使用同一种 API 网关,从而避免了不同解决方案之间的兼容问题。且一个成熟,生态强大的开源的 API 网关可以帮助用户解决很多场景的问题,即便不能覆盖全部的场景,这些 API 网关通常也允许用户通过多种不同的方式进行扩展,比如 Apache APISIX 允许用户通过使用 Go、Java、Python、Lua、WASM 等语言和技术进行扩展。</p> <p>然而这些开源 API 网关通常采用 Open Core 的开源策略,产品中的核心功能都开源了,但是面向企业级的管理能力,如可视化控制台、多集群管理、审计、SSO 等功能往往是收费的(集成在它们的商业产品中),这就导致如果用户不希望向他们付费,只能选择自研一套管理平台(也称为网关的控制面)来管理这些 API 网关。这可能意味着用户需要为此招聘一名全职的工程师负担起这部分的开发工作。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/18/sLMFgdLF_open-source-api-gw.png" alt="Custom Control Plane" referrerpolicy="no-referrer"></p> <h3>购买企业级 API 网关 SaaS 服务</h3> <p>如果开源的 API 网关在功能上满足需求,但是因为无法接受自研控制面的成本的话,用户也可以考虑联系这些开源软件的原厂(如 Apache APISIX 背后的原厂 API7.ai,Kong 背后的原厂 Kong inc,以及 Tyk 背后的原厂 Tyk inc 等),这些原厂往往会提供不同的企业级 API 网关产品和支持服务。尤其在“多云”和“混合云”的场景下,这些原厂相继都发布了他们的 SaaS 产品。API 网关的 SaaS 产品往往聚焦在管理侧,提供开箱即用的控制台,而不关心 API 网关实例具体部署在哪里(当然,某些 SaaS 服务也支持托管 API 网关实例,但这往往也会引入其他问题,比如代理 API 时的延迟)。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/18/llQTd7gb_saas-api-gateway.png" alt="SaaS" referrerpolicy="no-referrer"></p> <p>SaaS 服务通常会为每个租户托管一个用户独享的网关控制面,然后将用户部署的(或者 SaaS 服务托管的)网关实例进行连接(往往会使用 mTLS 等策略确保数据的安全性、隐私性),进而对它们进行管理。虽然购买 SaaS 服务需要花费一定的金钱,但是相比于招聘专职的工程师维护一个 API 网关和它的控制面,这些支出可能会更低。</p> <p>当然,采购 SaaS 服务需要谨慎,用户确认订单前,应该从下面几个方面对一个 SaaS 服务进行评估:</p> <ol> <li>这个 SaaS 服务是否能满足现在以及未来一段时间的使用需求?</li> <li>这个 SaaS 服务厂商是否专业诚信?他们拥有哪些用户案例?</li> <li>这个 SaaS 服务是否合规,符合 GDPR,拥有 SOC2 审计报告?</li> <li>这个 SaaS 服务是否有明确的 SLA 条款?</li> <li>这个 SaaS 服务是如何收费的?用户是否可以预估未来一年或者几年的支出?</li> </ol> <h3>完全自研</h3> <p>如果开源的 API 网关解决方案无法满足用户的实际场景,用户可能选择考虑完全自研专属于他们的 API 网关产品,但这往往费时费力,且网关本身的稳定性也是一个重大问题。研发一个 API 网关虽然不像实现一个关系型数据库或者浏览器那么难,但也不是一朝一夕就可以完成的,因此完全自研可以认为是“最坏的策略”。往往只能作为最后的选择。</p> <h2>四、总结</h2> <p>在云原生的大环境下,在“多云”和“混合云” 的场景下进行 API 的管理将会成为每一个组织发展时将面临的一个问题,甚至在没有采用“多云”和“混合云”之前就应该开始未雨绸缪。 尽管本文给出了几种不同的选择,但是需要谨记的是,它们之中没有“银弹”,用户应该根据业务的实际情况和发展方向选择一个“目前看来最合适的方案”。</p>

从 HTTP 到 gRPC:APISIX 中 etcd 操作的迁移之路

<h2>Apache APISIX 现有基于 HTTP 的 etcd 操作的局限性</h2> <p>etcd 在 2.x 版本的时候,对外暴露的是 HTTP 1 (以下简称 HTTP)的接口。etcd 升级到 3.x 版本后,其对外 API 的协议从普通的 HTTP 切换到了 gRPC。为了兼顾那些不能使用 gRPC 的特殊群体,etcd 通过 gRPC-gateway 的方式代理 HTTP 请求,以 gRPC 形式去访问新的 gRPC API。</p> <p>APISIX 开始用 etcd 的时候,用的是 etcd v2 的 API。从 2020 年的 APISIX 2.0 版本起,我们把要求的 etcd 版本升级到 3.x。etcd 对 HTTP 的兼容帮了我们很大的忙,这样就不用花很大心思去重新实现操作 etcd 的方式了,只需调整下新的一组 API 的调用方式和响应处理。然而在实践过程中,我们也发现了跟 etcd 的 HTTP API 相关的一些问题。事实上,拥有 gRPC-gateway 并不意味着能够完美支持 HTTP 访问,这里还是有些细微的差别。</p> <p>我把过去几年来在 etcd 上遇到的相关问题列了一下:</p> <ol> <li>在某些情况下,etcd 默认没有启用 gRPC-gateway。由于维护者的疏忽,在某些平台上,etcd 在配置时没有默认启用 gRPC-gateway,所以我们不得不在文档中添加一个注释,以检查 etcd 当前是否启用了 gRPC-gateway。具体可参考 https://github.com/apache/apisix/pull/2940。</li> <li>gRPC 默认将响应限制在 4MB 以内。etcd 在其提供的 sdk 中解除了这一限制,但忘记了在 gRPC-gateway 中解除。因此,官方的 etcdctl(使用了 sdk)工作正常,但 APISIX 却无法正常访问。具体可参考 https://github.com/etcd-io/etcd/issues/12576。</li> <li>同样的问题 —— 这次是发生在同一连接的最大请求数上。Go 的 HTTP2 实现有一个 MaxConcurrentStreams的配置,控制单个客户端能同时发送的请求数,默认是 250。正常情况下,哪个客户端会同时发送超过 250 个请求呢?所以 etcd 一直沿用这一配置。然而 gRPC-gateway 这个代理所有 HTTP 请求到本机的 gRPC 接口的“客户端”,却有可能超出这一限制。具体可参考 https://github.com/etcd-io/etcd/issues/14185。</li> <li>etcd 在开启 mTLS 后,会用同一个证书来作为 gRPC-gateway 的服务端证书,兼 gRPC-gateway 访问 gRPC 接口时的客户端证书。如果该证书上启用了 server auth 的拓展,但是没有启用 client auth 的拓展,那么会导致证书检验出错。直接用 etcdctl 访问不会有这个问题,因为该证书在这种情况下不会作为客户端证书使用。具体可参考 https://github.com/etcd-io/etcd/issues/9785。</li> <li>etcd 在开启 mTLS 后,允许针对证书里面的用户信息配置安全策略。如上所述,gRPC-gateway 在访问 gRPC 接口时用的是固定的一个客户端证书,而非一开始访问 HTTP 接口所用的证书信息。这个功能自然就没办法正确工作了。具体可参考 https://github.com/apache/apisix/issues/5608。</li> </ol> <p>造成上述问题的原因可以归纳为两点:</p> <ol> <li>gRPC-gateway(或许还有其他将 HTTP 转换为 gRPC 的尝试)并不是万能的。</li> <li>etcd 的开发者没有对 HTTP 到 gRPC 的路径给予足够的重视。毕竟他们最大的用户,Kubernetes,并不会使用它。</li> </ol> <p>要想从本源上解决这一问题,我们需要直接通过 gRPC 来操作 etcd,这样就不用走为了兼容而保留的 HTTP 路径。</p> <h2>克服各种挑战,迁移到 gRPC</h2> <h3>跳过 lua-protobuf 的 bug</h3> <p>在迁移过程中,我们遇到的第一个问题,是一个预想不到的第三方库的 bug。像绝大多数 OpenResty 应用一样,我们采用 lua-protobuf 来完成 protobuf 的 decode/encode。在引入 etcd 的 proto 文件之后,我们发现 Lua 代码中会出现偶发的奔溃,报错说遇到了 "table overflow"。由于这个崩溃不能稳定复现,我的第一反应是寻找最小可复现的例子。但是有趣的是,如果单独使用 etcd 的 proto 文件,那么无论如何都无法复现该问题。这个崩溃似乎只能出现在 APISIX 运行的时候,真是神奇。通过一些调试,我把问题锁定到了 lua-protobuf 在解析 proto 文件的 oneof 字段时的逻辑。lua-protobuf 在解析时会尝试预分配 table 大小,而分配的大小是按照某个值计算过来的。有一定几率,这个值会是个负数,然后 LuaJIT 在分配时把这个数转成很大的正数,结果导致了 "table overflow" 错误。我向作者报告了该问题,并在我们内部维护了一个带 workaround 的 fork。lua-protobuf 作者反应很迅速,次日就提供了一个修复,并在几天后发布了新的版本。原来 lua-protobuf 在清理不再使用的 proto 文件时,曾经漏了减少某些字段,导致在后续处理 oneof 时会得到一个不合理的负数。之所以是偶现的问题,之所以无法在单独使用 etcd 的 proto 文件时复现,都是因为少了“清理”这一关键的前置操作。</p> <h3>向 HTTP 的行为对齐</h3> <p>世间鲜有东西是从头开始的,往往不得不从当前继承一些负担。在迁移过程中,我发现一个问题,就是现有的 API 返回的并不是一个简单的请求执行结果,而是个包含 status 和 body 的 HTTP response,然后由调用方去处理这个 HTTP response。这么一来,gRPC 的响应结果就需要套上一个 HTTP response 的壳。不然调用方就需要改动很多处地方来适配新的响应格式。尤其考虑到旧的基于 HTTP 的 etcd 操作也需要同时支持,虽然兼容 HTTP response 的套壳行为并非我所欲,但是还是得向现实低头妥协。除了要加壳之外,我们还需要对 gRPC 响应做一些裁剪。比如在没有对应数据时,HTTP 不会返回任何数据,而 gRPC 会返回一个空 table。裁剪结果嘛,自然都是向 HTTP 的行为靠齐。</p> <h3>从短连接到长连接</h3> <p>在基于 HTTP 的 etcd 操作中,APISIX 都是采用短连接,所以不需要考虑诸如连接管理的问题。反正用的时候发起新连接,用完 close 即可。但 gRPC 就不能这么操作了。迁移到 gRPC 的一大目的就是为了达到多路复用,如果每次操作都创建新的 gRPC 连接,那么就无从实现这一目的。这里需要感谢 gRPC 功能的内核 gRPC-go 自带了连接管理的能力,面对连接中断可以自动重连。这样判断是否需要复用连接,在 APISIX 这一层就只需考虑业务需求。APISIX 的 etcd 操作可以分为两类,一类是去对 etcd 的数据做增删改查;另一类是从控制面同步配置。虽然理论上这两种 etcd 操作可以共享同一个 gRPC 连接,但是出于权责划分的考虑,我们决定把它们分成两个连接。对于增删改查的连接,由于 APISIX 启动时和启动后的连接需要分开对待,所以在获取新的连接时加了个判断,如果当前连接是启动时创建且目前需要的是启动后的连接,那么会关闭当前连接并创建新的连接。对于配置同步,我开发了新的同步方式,让每一种资源都采用当前连接下面的一个 stream 来 watch etcd。</p> <h2>迁移到 gRPC 后,我们得到了什么样的好处</h2> <p>迁移到 gRPC 后,一个显而易见的好处是操作 etcd 所需的连接大大减少。原来通过 HTTP 操作 etcd 时,APISIX 都是采用短连接。而且在同步配置时,每种资源都会有一个单独的连接。在切换到 gRPC 之后,我们可以用上 gRPC 的多路复用功能,每个资源都只使用单个 stream,而不是完整的连接。这样一来,连接数就不再随着资源数的增加而增加。考虑到 APISIX 的后续开发将会引入更多的资源类型,比如当前最新的 3.1 版就新增了 secrets,采用 gRPC 带来的连接数的减少将会越来越明显。</p> <p>使用 gRPC 同步时,每个进程只有一条用于配置同步的连接(如果开启了 stream 子系统,那么是两条)。在下图中我们能看到两个进程共有四个连接,其中两个是配置同步,还有一个连接由 Admin API 使用,剩下一个连接是特权 agent 上报 server info。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/17/kjR7F649_FjNz1o9UcAIf6ac.png" alt="gRPC uses much less connections" referrerpolicy="no-referrer"></p> <p>作为对比,下图是保持其他配置不变,使用原有的配置同步方式所需的连接数。具体有多少我就不数了。另外这些连接还是短连接。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/17/h0mcaE4F_FjN11JoVQAAnax-.png" alt="too many connections" referrerpolicy="no-referrer"></p> <p>这两个配置的区别,仅在于是否启用了 gRPC 来做 etcd 操作:</p> <pre><code class="language-yaml"> etcd: use_grpc: true host: - "http://127.0.0.1:2379" prefix: "/apisix" ... </code></pre> <p>除了连接数的减少外,使用 gRPC 来直接访问 etcd 而非绕道 gRPC-gateway,能够解决文中一开始提到的关于 mTLS 鉴权等一系列受限于架构的问题。此外,我们也相信使用 gRPC 后遇到更少的问题,因为 Kubernetes 就是通过 gRPC 来操作 etcd 的,如果有问题,在 Kubernetes 社区应该会更早地发现。</p> <p>当然,作为一个新生事物,APISIX 通过 gRPC 操作 etcd 时不免会有一些新的问题。毕竟我们之所以发现了许多 HTTP 路径下的问题,是因为我们对 HTTP 用得比较多。所以目前我们还是默认采用原有的基于 HTTP 的方式来操作 etcd。不过用户可以自行在 <code>config.yaml</code> 中配置 etcd 下的 <code>use_grpc</code> 为 true,尝试一下新的方式是否更优。我们也会听取各路反馈意见,继续完善基于 gRPC 的 etcd 操作,并持续积累更多的经验。在后续的某个时候,当我们判断新方式足够成熟时,就会把它确定为默认的途径。</p>

Apache APISIX Lua 动态调试插件(inspect)详解

<h2>为什么需要 Lua 动态调试插件?</h2> <p>Apache APISIX 有很多 Lua 代码,如何在运行时不触碰源代码的情况下,检查代码里面的变量值?</p> <p>修改 Lua 源码来调试有如下缺点:</p> <ul> <li>生产环境不允许也不应该修改源码</li> <li>修改源码需要 reload,使得业务功能失效</li> <li>容器环境难以修改源码</li> <li>产生的临时代码容易忘记回滚,导致维护问题</li> </ul> <p>很多时候我们不仅仅需要在函数开始或结束的时候去检查变量,而且需要在满足一定条件,例如某个循环体被循环到了一定次数, 或者某个条件判断为真的时候我们才查看变量值,并且也不仅仅是简单打印变量值,有时候还可能需要将相关信息发送到外围系统。 并且,这个过程如何做到动态化呢?而且,开启调试后,能否不影响程序运行的性能呢?</p> <p>Lua 动态调试插件就是辅助你完成以上需求的插件,该插件被命名为 <code>inspect</code> 插件。</p> <ul> <li>断点处理可定制</li> <li>断点设置动态化</li> <li>多个断点</li> <li>断点可被定义为只生效一次</li> <li>可控制性能影响范围</li> </ul> <h2>插件原理</h2> <p>它充分利用了 Lua 提供的 Debug API 来实现功能。解释器模式执行的每一个字节码都可以对应到它所属的文件以及行号,我们只需要判断行号是否等于期望值,然后执行我们定义的断点函数,对该行对应的上下文信息,包括 upvalue ,局部变量,还有一些元信息,例如堆栈,进行处理即可。</p> <p>APISIX 使用的是 Lua 的 JIT 实现:LuaJIT,很多热点代码路径会被编译成机器码执行,而它们是不受 Debug API 的影响的,所以我们需要在开启断点前清空 JIT 缓存。关键就在这里了,我们可以选择只清空某个具体 Lua 函数的 JIT 缓存,减小对全局性能的影响。一个程序运行起来,会有很多 JIT 编译代码块,在 LuaJIT 里被称为 trace,这些 trace 跟 Lua 函数是关联起来的,一个 Lua 函数可能包括多个 trace ,指代函数内不同的热点路径。</p> <p>对于全局函数、模块级别的函数,我们可以指定它们的函数对象,清空它们的 JIT 缓存。但是如果某行号对应的是其他函数类型,例如匿名函数,我们无法在全局获取函数的对象,那么只能清空所有 JIT 缓存了。在调试开启期间,新的 trace 无法被生成,但是已有的未被清理的 trace 还继续运行,所以只要控制的好,程序性能不会受到影响,因为一个已经运行很久的线上系统,基本不会有新 trace 的生成。当调试结束后,也就是所有断点都被撤销后,系统会恢复正常的 JIT 模式,被清理掉的 JIT 缓存,一旦重新进入热点,会被重新生成 trace。</p> <h2>安装与配置</h2> <p>该插件默认被启用。</p> <p>配置好 <code>conf/confg.yaml</code> 启用插件:</p> <pre><code class="language-yaml">plugins: ... - inspect plugin_attr: inspect: delay: 3 hooks_file: "/usr/local/apisix/plugin_inspect_hooks.lua" </code></pre> <p>插件默认每隔3秒从文件 <code>/usr/local/apisix/plugin_inspect_hooks.lua</code> 读取断点定义,想调试就编辑该文件即可。</p> <p>建议创建软链接到该路径,这样比较方便地存档不同历史版本的断点文件。</p> <p>注意每次该文件的更改时间有变,插件会清空所有旧的断点,并且启用断点文件所定义的所有新断点。断点将在所有工作进程生效。</p> <p>一般情况下不需要删除该文件,因为定义断点的时候,可以定义什么时候撤销断点。</p> <p>删除文件会取消所有工作进程的所有断点。</p> <p>断点的启停都会通过 <code>WARN</code> 日志级别打印日志。</p> <h2>定义断点</h2> <pre><code class="language-lua">require("apisix.inspect.dbg").set_hook(file, line, func, filter_func) </code></pre> <ul> <li><code>file</code> 文件名,可以是任何无歧义的文件名部分,可包含路径</li> <li><code>line</code> 文件的行号,注意断点跟行号是密切挂钩的,所以如果代码变了,行号就得跟着变。</li> <li><code>func</code> 要清除哪个函数的 trace,如果为 nil,则清除 luajit vm 里面所有 trace</li> <li><code>filter_func</code> 处理该断点的自定义 Lua 函数 <ul> <li>函数的入参为一个 <code>table</code>,包含以下内容 <ul> <li><code>finfo</code>: <code>debug.getinfo(level, "nSlf")</code>的返回值</li> <li><code>uv</code>: upvalues hash table</li> <li><code>vals</code>: local variables hash table</li> </ul> </li> <li>函数的返回值为 <code>true</code>,则该断点自动注销,返回为 <code>false</code>,则该断点继续生效</li> </ul> </li> </ul> <p>例子:</p> <pre><code class="language-lua">local dbg = require "apisix.inspect.dbg" dbg.set_hook("limit-req.lua", 88, require("apisix.plugins.limit-req").access, function(info) ngx.log(ngx.INFO, debug.traceback("foo traceback", 3)) ngx.log(ngx.INFO, dbg.getname(info.finfo)) ngx.log(ngx.INFO, "conf_key=", info.vals.conf_key) return true end) dbg.set_hook("t/lib/demo.lua", 31, require("t.lib.demo").hot2, function(info) if info.vals.i == 222 then ngx.timer.at(0, function(_, body) local httpc = require("resty.http").new() httpc:request_uri("http://127.0.0.1:9080/upstream1", { method = "POST", body = body, }) end, ngx.var.request_uri .. "," .. info.vals.i) return true end return false end) --- more breakpoints ... </code></pre> <p>注意到 demo 这个断点,它将一些信息整理后发送到外部的服务器上,使用的 <code>resty.http</code> 库是基于 <code>cosocket</code> 的异步库。</p> <p><strong>凡是调用 OpenResty 的异步 API ,必须使用 timer 延迟发送,因为在断点上执行函数是同步阻塞的,不会再返回到 nginx 的主程序做异步处理,所以需要延后发送。</strong></p> <h2>使用示例</h2> <h3>根据请求体的内容来决定路由</h3> <p>假设我们有个需求,如何设置让某个路由仅接受请求体中携带了 <code>APISIX: 666</code> 的 POST 请求?</p> <p>路由配置里面有个 <code>vars</code> 字段,是用来检查 nginx 变量的值来判断是否匹配该路由的, 而 <code>$request_body</code> 则是 nginx 提供的变量,包含请求体的值,那我们可以利用这个变量来实现我们的需求?</p> <p>让我们来尝试一下,先配置一下路由:</p> <pre><code>curl http://127.0.0.1:9180/apisix/admin/routes/var_route \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "uri": "/anything", "methods": ["POST"], "vars": [["request_body", "~~", "APISIX: 666"]], "upstream": { "type": "roundrobin", "nodes": { "httpbin.org": 1 } } }' </code></pre> <p>然后我们尝试一下:</p> <pre><code>curl http://127.0.0.1:9080/anything {"error_msg":"404 Route Not Found"} curl -i http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.' HTTP/1.1 404 Not Found Date: Thu, 05 Jan 2023 03:53:35 GMT Content-Type: text/plain; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Server: APISIX/3.0.0 {"error_msg":"404 Route Not Found"} </code></pre> <p>奇怪,为什么匹配不上这个路由呢?</p> <p>我们再查看一下 NGINX 对该变量的文档说明:</p> <blockquote> <p>The variable’s value is made available in locations processed by the proxy_pass, fastcgi_pass, uwsgi_pass, and scgi_pass directives when the request body was read to a memory buffer.</p> </blockquote> <p>也就是说,使用该变量前需要先读取 request body 。</p> <p>那是不是匹配路由的时候,这个变量为空呢?我们可以使用 <code>inspect</code> 插件来验证一下。</p> <p>我们找到了匹配路由的代码行:</p> <p><code>apisix/init.lua</code></p> <pre><code class="language-lua">... api_ctx.var.request_uri = api_ctx.var.uri .. api_ctx.var.is_args .. (api_ctx.var.args or "") router.router_http.match(api_ctx) local route = api_ctx.matched_route if not route then ... </code></pre> <p>我们就在 515 行,也就是 <code>router.router_http.match(api_ctx)</code> 这行验证一下变量 <code>request_body</code> 吧。</p> <h4>设置断点</h4> <p>编辑文件 <code>/usr/local/apisix/example_hooks.lua</code>:</p> <pre><code class="language-lua">local dbg = require("apisix.inspect.dbg") dbg.set_hook("apisix/init.lua", 515, require("apisix").http_access_phase, function(info) core.log.warn("request_body=", info.vals.api_ctx.var.request_body) return true end) </code></pre> <p>创建软链接到断点文件路径:</p> <pre><code class="language-bash">ln -sf /usr/local/apisix/example_hooks.lua /usr/local/apisix/plugin_inspect_hooks.lua </code></pre> <p>检查日志看看确认断点生效:</p> <pre><code>2023/01/05 12:02:43 [warn] 1890559#1890559: *15736 [lua] init.lua:68: setup_hooks(): set hooks: err: true, hooks: ["apisix\/init.lua#515"], context: ngx.timer </code></pre> <p>再触发一次路由匹配:</p> <pre><code>curl -i http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.' </code></pre> <p>查看日志:</p> <pre><code>2023/01/05 12:02:59 [warn] 1890559#1890559: *16152 [lua] [string "local dbg = require("apisix.inspect.dbg")..."]:39: request_body=nil, client: 127.0.0.1, server: _, request: "POST /anything HTTP/1.1", host: "127.0.0.1:9080" </code></pre> <p>果然,<code>request_body</code> 是空的!</p> <h4>解决方案</h4> <p>既然我们知道需要读取请求体才能用 <code>request_body</code> 变量,那么我们就不能通过 <code>vars</code> 来做了,那我们可以通过路由里面的 <code>filter_func</code> 字段来实现需求。</p> <pre><code class="language-lua">curl http://127.0.0.1:9180/apisix/admin/routes/var_route \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "uri": "/anything", "methods": ["POST"], "filter_func": "function(_) return require(\"apisix.core\").request.get_body():find(\"APISIX: 666\") end", "upstream": { "type": "roundrobin", "nodes": { "httpbin.org": 1 } } }' </code></pre> <p>验证一下:</p> <pre><code>curl http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.' { "args": {}, "data": "", "files": {}, "form": { "hello, APISIX: 666.": "" }, "headers": { "Accept": "*/*", "Content-Length": "19", "Content-Type": "application/x-www-form-urlencoded", "Host": "127.0.0.1", "User-Agent": "curl/7.68.0", "X-Amzn-Trace-Id": "Root=1-63b64dbd-0354b6ed19d7e3b67013592e", "X-Forwarded-Host": "127.0.0.1" }, "json": null, "method": "POST", "origin": "127.0.0.1, xxx", "url": "http://127.0.0.1/anything" } </code></pre> <p>问题解决!</p> <h3>打印一些被日志级别屏蔽的日志</h3> <p>生产环境一般不会开启 <code>INFO</code> 级别的日志,但是有时候我们又需要检查一些详细信息,那怎么办呢?</p> <p>我们一般不会直接设置 <code>INFO</code> 级别然后 reload,因为这样做有两个缺点:</p> <ul> <li>日志太多,影响性能和加大检查难度</li> <li>reload 导致长连接被断开,影响在线流量</li> </ul> <p>一般我们只需要检查具体某个点的日志,例如我们都知道 APISIX 使用 etcd 作为配置分发数据库,那么可否看看什么时候路由配置被增量更新到了数据面呢?更新了什么具体数据呢?</p> <p><code>apisix/core/config_etcd.lua</code></p> <pre><code class="language-lua">local function sync_data(self) ... log.info("waitdir key: ", self.key, " prev_index: ", self.prev_index + 1) log.info("res: ", json.delay_encode(dir_res, true), ", err: ", err) ... end </code></pre> <p>增量同步的lua函数是 <code>sync_data()</code>,但是它是通过 <code>INFO</code> 级别来打印从 etcd watch 到的增量数据的。</p> <p>那么我们来试一下使用 inspect plugin 来显示一下?只显示路由资源的变化。</p> <p>编辑 <code>/usr/local/apisix/example_hooks.lua</code> :</p> <pre><code class="language-lua">local dbg = require("apisix.inspect.dbg") local core = require("apisix.core") dbg.set_hook("apisix/core/config_etcd.lua", 393, nil, function(info) local filter_res = "/routes" if info.vals.self.key:sub(-#filter_res) == filter_res and not info.vals.err then core.log.warn("etcd watch /routes response: ", core.json.encode(info.vals.dir_res, true)) return true end return false end) </code></pre> <p>这个断点处理函数的逻辑很好表达了过滤能力,如果 watch 的 <code>key</code> 是 <code>/routes</code>,以及 <code>err</code> 为空的情况下,就打印 etcd 返回的数据,并且打印一次就够了,就取消断点。</p> <p>注意 <code>sync_data()</code> 是局部函数,所以无法获取它的引用,我们只能设置 <code>set_hook</code> 的第三个参数为 <code>nil</code>,这样做的副作用就是它会清空所有 <code>trace</code>。</p> <p>上面例子我们已经创建了软链接,所以编辑后保存文件即可。等几秒钟后,断点就会被启用,可观察日志确认。</p> <p>检查日志,我们可以得到我们需要的信息,而这些信息用 <code>WARN</code> 日志级别打印,并且也显示了我们在数据面获取到 etcd 增量数据的时间。</p> <pre><code>2023/01/05 14:33:10 [warn] 1890562#1890562: *231311 [lua] [string "local dbg = require("apisix.inspect.dbg")..."]:41: etcd watch /routes response: {"headers":{"X-Etcd-Index":"24433"}, "body":{"node":[{"value":{"uri":"\/anything", "plugins":{"request-id":{"header_name":"X-Request-Id","include_in_response":true,"algorithm":"uuid"}}, "create_time":1672898912,"status":1,"priority":0,"update_time":1672900390, "upstream":{"nodes":{"httpbin.org":1},"hash_on":"vars","type":"roundrobin","pass_host":"pass","scheme":"http"}, "id":"reqid"},"key":"\/apisix\/routes\/reqid","modifiedIndex":24433,"createdIndex":24429}]}}, context: ngx.timer </code></pre> <h2>结论</h2> <p>Lua 动态调试是很重要的辅助功能。我们可以通过 APISIX <code>inspect</code> 插件来做很多事情,例如:</p> <ul> <li>排查问题,定位原因</li> <li>打印一些被屏蔽的日志,按需获取各种信息</li> <li>通过调试来学习 Lua 代码</li> </ul> <p>更多详情请查阅<a href="https://apisix.apache.org/docs/apisix/plugins/inspect/">相关文档介绍</a>。</p>

如何在 API Gateway 中使用 Secret Manager 来存储相关密钥?

<h2>什么是 Secret</h2> <p>在 IT 数据环境中,Secret 是数字特权凭据,通常是系统和应用程序用于身份验证或作为加密算法输入的凭据。比如程序连接数据库所需要的密码、加密通信用到的证书私钥等,都属于 Secret 的范畴。常见的 Secret 类型包括:</p> <ul> <li>加密密钥</li> <li>云服务访问凭据</li> <li>应用程序编程接口 (API) 密钥</li> <li>访问令牌</li> <li>SSH(安全外壳)密钥</li> </ul> <h2>Secret Manager</h2> <p>Secret Manager 用于对 Secret 进行全生命周期的存储、检索、轮换和审计。HashiCorp Vault 是目前最常见的 Secret Manager 服务之一,其工作原理如下(图片来自 HashiCorp 官方):</p> <p><img src="https://static.apiseven.com/uploads/2023/01/11/7WAe7EeD_assets.png" alt="secret manager" referrerpolicy="no-referrer"></p> <p>除了 HashiCorp Vault 之外, AWS、Google、Azure 等云厂商也都提供了 Secret Manager 服务。</p> <p>使用 Secret Manager 构建应用程序除了能提升安全性之外,还具有以下功能:</p> <ul> <li>允许轻松地跨多台机器更新 Secret,应用程序无感。</li> <li>开发人员无需过多关注 Secret 的存储等,可以将关注点集中于业务逻辑本身。</li> <li>Secret 轮转和吊销都无需重新部署或中断活动应用程序。</li> <li>Secret Manager 提供了灵活且详细审计日志功能,可以跟踪到所有用户的历史访问记录,能更轻松地满足审核和合规性要求。</li> </ul> <h2>在 API Gateway 中使用 Secret Manager</h2> <p>API Gateway 作为业务流量的入口,往往包含了大量的 Secret 信息,例如 API 密钥、用于鉴权认证的 token 等,因此在 API Gateway 中集成 Secret Manager 具有重要意义。</p> <h3>API Gateway 使用 Secret 场景</h3> <p>Key Auth 是 API Gateway 中一种简单的身份认证方式。用户需要预设一个密钥,当客户端请求 API 时,将预设的密钥写入对应请求头,或者作为请求参数访问即可通过认证。然而,密钥泄露的情况非常普遍,密钥通常被储存在配置文件中,或作为变量被储存在代码中。如果没有妥善保存,密钥甚至会出现在 GitHub 等公开可见的代码库中,从而对安全构成重大威胁。为了防止密钥泄漏造成大的危害,管理员通常也会选择定期更新这些密钥,对于一些静态密钥,逐个机器更新配置文件,也将对服务的稳定性带来很大的危害。</p> <h3>API Gateway 集成 Secret Manager</h3> <p>在 API Gateway 中集成 Secret Manager后,可以将 Key Auth 认证预设的密钥存储在 Secret Manager 服务中。能有效解决密钥泄漏带来的一系列问题。API Gateway 只是密钥的使用方,真正的密钥值保存在外部 Secret Manager 服务中,API Gateway 中保存的只是密钥的引用,即使攻破了 API Gateway,也无法从代码或配置文件中获取到真正的密钥。另外,如果需要更新密钥的值,只需要在 Secret Manager 中更新即可,无需对 API Gateway 的代码或配置做任何修改,这也有效避免了 API Gateway 的重启。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/08/RQXcA3iO_1.png" alt="api gateway secret manager" referrerpolicy="no-referrer"></p> <p>下面将以 Apache APISIX 为例,展示在 API Gateway 中何如使用 Secret Manager 来存储 Key Auth 的预设密钥。</p> <h3>Apache APISIX 在 Secret Manager 中的实践</h3> <p>Apache APISIX 是一个动态、实时、高性能的 API 网关, 提供负载均衡、动态上游、灰度发布、精细化路由、限流限速、服务降级、服务熔断、身份认证、可观测性等数百项功能。</p> <p>在 3.1.0 版本中 Apache APISIX 引入了 APISIX Secret 资源用来集成不同的 Secret Manager。其工作时序如下:</p> <p><img src="https://static.apiseven.com/2023/01/04/63b54bdfa779c.png" alt="apisix secret manager" referrerpolicy="no-referrer"></p> <p>下面使用 Vault 作为 Secret Manager 服务,以 Key Auth 身份认证为例,展示在 Apache APISIX 中如何使用 Secret Manager 保存密钥。</p> <h4>在 Vault 中创建密钥</h4> <p>在创建密钥之前,需要先启动 Vault 服务,由于本节分享了在 Apache APISIX 生态系统中使用 Vault 的最佳实践,关于 Vault 自身的配置不作详细描述,可以参考<a href="https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1">这里</a>。</p> <p>在 Vault 中创建对应的密钥,可以使用如下命令:</p> <pre><code class="language-shell">vault secrets enable -version=1 -path=apisix kv vault kv put apisix/jack auth-key=secret-key </code></pre> <p>上述命令在 Vault 的 apisix/jack 路径下创建了名为 auth-key 的密钥,值为 <code>secret-key</code>。</p> <h4>在 APISIX 中配置</h4> <p>首先,通过 Admin API 添加 Secret 资源,配置 Vault 的地址等连接信息:</p> <pre><code class="language-shell">curl http://127.0.0.1:9180/apisix/admin/secrets/vault/gateway \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "https://127.0.0.1:8200", "prefix": "apisix", "token": "hvs.chjDFWvZRcihoq2vpSsVenmR" }' </code></pre> <p>然后,使用 <code>key-auth</code> 插件创建一个消费者,为对应的路由或服务执行 <a href="https://docs.api7.ai/apisix/getting-started/key-authentication">Key Auth</a> 认证。其中 key 字段引用 APISIX Secret 资源:</p> <pre><code class="language-shell">curl http://127.0.0.1:9180/apisix/admin/consumers \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "jack", "plugins": { "key-auth": { "key": "$secret://vault/geteway/jack/auth-key" } } }' </code></pre> <p>通过上面两步操作,当用户请求命中 key-auth 插件时,会通过 Secret 组件提供的接口调用用户配置的 Secret Manager,获取到 key 在 Vault 中的真实值,如果没有找到 key 的值,插件会记录错误,并且无法执行 Key Auth 验证。</p> <p>另外,Apache APISIX 将环境变量扩展成了一种简单的 Secret Manager 服务,APISIX 连接 Vault 的 token 也可以保存环境变量中。在 APISIX 启动之前,可以通过下面的命令设置环境变量:</p> <pre><code class="language-shell">export VAULT_TOKEN="root" </code></pre> <p>在添加 Secret 资源时引用环境变量:</p> <pre><code class="language-shell">curl http://127.0.0.1:9180/apisix/admin/secrets/vault/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "https://127.0.0.1:8200", "prefix": "apisix", "token": "$ENV://VAULT_TOKEN" }' </code></pre> <p>通过上面步骤,就可以实现将 Key Auth 认证需要的密钥存储在 Vault 中,而不是以明文的形式在配置插件时展示。</p> <h2>总结</h2> <p>在 API Gateway 中,包含大量的 Secret 信息,使用 Secret Manager 来管理 Secret 能保证所有 Secret 使用过程,不会有任何明文 Secret 信息出现在 API Gateway 中,有效提升提升了 API Gateway 的安全性和稳定性。Apache APISIX 作为全世界最活跃的开源 API Gateway,在 Secret Manager 上也有很好的支持,本文也以为 Key Auth 身份认证为例,展示了如何在 APISIX 中使用 Secret Manager 来管理密钥信息。</p>

借助 APISIX Ingress,实现与注册中心的无缝集成

<h2>云原生场景下是否需要服务发现</h2> <h3>背景</h3> <p>微服务架构是当前最为流行的应用架构之一。 应用被拆分为多个服务组件,通过相互配合共同完成业务的具体逻辑和功能。</p> <p>随着应用规模的增加和微服务拆分粒度的不同,一套系统内会包含很多个服务组件。 要让这些组件之间可以很好的协同,并且能彼此识别到,通常都需要引入服务注册和发现组件。</p> <p>之前我们写了一篇文章专门来介绍微服务架构中的服务发现, <a href="https://api7.ai/blog/what-is-service-discovery-in-microservices">What Is Service Discovery in Microservices - API7.ai</a></p> <p>简单来说,<strong>通过引入了服务发现组件,微服务架构中的组件,只需要关注其他组件的名称即可,而无需关注其他组件的部署位置,或者部署 IP 等信息。 通过服务发现组件可以动态的进行获取。</strong></p> <h3>Kubernetes 中的服务发现</h3> <p>在 Kubernetes 环境中,Pod 是最小的调度单元。 而且在 Kubernetes 中,Pod 的 IP 不具备持久性,常常会发生变更。彼此协同的组件,一旦 IP 发生变更,很难再很好的配合工作。</p> <p>所以将业务部署在 Kubernetes 环境中,如何能适配这种动态的环境就显的更加重要了。</p> <p>Kubernetes 考虑到了这方面的诉求,它默认提供了基于 DNS 的服务发现机制。</p> <p>Kubernetes 中有一个概念叫作 Service,它类似于反向代理的作用。客户端仅仅需要知道 Service 的存在,而无需关注它背后的 Pod 的变化,每个 Service 都会分配一个持久化的 ClusterIP 。 这样在业务组件的 Pod IP 发生变化的时候,其他组件仍然可以正常的通过 Service 的 ClusterIP 进行协同。</p> <p>同时,Kubernetes 中的 Service 会自动的在集群内的 DNS 中添加一条 A 记录。</p> <p>它的形式类似于:</p> <pre><code>my-svc.my-namespace.svc.cluster-domain.example </code></pre> <p>客户端可以进行相同 namespace 或者跨 namespace 的解析,这就大大的方便了需要协同工作的组件之间进行服务发现。</p> <p>但是,对于传统的微服务架构,正如本文开头提到的那样,通常是利用服务注册和发现组件来实现服务间协同配合的。 想要完全适配 Kubernetes 环境中基于 DNS 的这种服务发现机制,需要一定的改造成本。</p> <p>所以,如果想要较低的改造成本,那就需要继续保持原有的服务注册和发现机制。 在这个大前提下,<strong>迁移至 Kubernetes 中的服务,如何对外暴露给客户端使用呢?</strong></p> <h2>APISIX Ingress 如何与注册中心联动</h2> <p>APISIX Ingress 是 Kubernetes 中的一种 Ingress Controller 实现,使用 APISIX 作为数据面代理组件, 支持通过 Ingress,自定义 CRD,Gateway API 等方式进行代理规则的配置。 同时也提供了与服务注册和发现组件的集成,可以方便的与服务发现组件进行联动。 将注册在其中的服务代理出来,提供给客户端使用。</p> <p>这一节将具体进行介绍。</p> <h3>APISIX 对服务发现的支持</h3> <p>APISIX 是一个高性能,全动态的云原生 API 网关,提供了 80+ 开箱即用的插件,涵盖了绝大多数用户的使用场景,其中一个很优秀的能力就是与服务发现组件的集成。</p> <p>APISIX 可以和以下服务发现组件进行集成使用:</p> <ul> <li>Consul</li> <li>DNS</li> <li>Eureka</li> <li>Nacos</li> </ul> <p>仅需要在 APISIX 的配置文件中添加如下配置即可(以 DNS 为例):</p> <pre><code class="language-yaml">discovery: dns: servers: - "127.0.0.1:8600" # use the real address of your dns server </code></pre> <p>这样,在配置上游的时候,APISIX 便可通过服务发现组件动态的解析出实际的上游地址信息,并进行请求的代理。</p> <h3>APISIX Ingress 如何做</h3> <p>APISIX Ingress 使用 APISIX 作为数据面代理组件。 再进行与服务发现组件集成的时候,我们考虑了两种模式。</p> <ul> <li>控制面集成:在 Ingress Controller 中配置服务发现组件,并进行配置的解析,将最终结果发送至 APISIX 进行代理;</li> <li>数据面集成: 在 APISIX 数据面配置服务发现组件,代理时,由数据面进行配置解析和代理;</li> </ul> <p>这两种方案各有优势,但考虑到配置的实时更新,还有方案的成熟度,我们最终选择了数据面集成的方案。 用户使用这种方案时,可以以更加低成本的方式进行接入,而且这种方案已经非常成熟了,经过了大量的生产验证。</p> <p>在 APISIX Ingress 中如何使用这种方案呢?</p> <p>首先确保在 APISIX 的配置中已经包含了正确的服务发现组件的配置,以下用 DNS 为例:</p> <pre><code class="language-yaml">discovery: dns: servers: - "10.96.0.10:53" </code></pre> <p>创建 ApisixUpstream 资源,它的 <code>discovery</code> 相关的配置根据实际情况进行修改,比如此处设置了 <code>type: dns</code> 和具体要代理的 <code>serviceName</code>:</p> <pre><code class="language-yaml"># httpbin-upstream.yaml apiVersion: apisix.apache.org/v2 kind: ApisixUpstream metadata: name: httpbin-upstream spec: discovery: type: dns serviceName: httpbin.default.svc.cluster.local </code></pre> <p>最后创建一个 ApisixRoute 资源,它的 <code>upstreams</code> 引用刚才创建的 ApisixUpstream 资源即可:</p> <pre><code class="language-yaml"># httpbin-route.yaml apiVersion: apisix.apache.org/v2 kind: ApisixRoute metadata: name: httpbin-route spec: http: - name: rule1 match: hosts: - local.httpbin.org paths: - /* upstreams: - name: httpbin-upstream </code></pre> <p>将上述资源都正确创建后,通过 <code>local.httpbin.org</code> 访问,即可访问到 DNS 中已经注册了的 <code>httpbin.default.svc.cluster.local</code> 了。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/11/7hYYdhji_service-discovery.png" alt="how client make requests" referrerpolicy="no-referrer"></p> <h2>收益和展望</h2> <p>通过这种集成,对于已经使用了服务注册发现组件的用户场景,可以非常方便的通过 APISIX Ingress 将其中的服务暴露给客户端使用。</p> <p>大多数 Ingress Controller 都是没有提供这种集成方案的,这也可以增加 APISIX Ingress 的应用场景。</p> <p>希望 APISIX Ingress 可以更好的满足用户实际业务场景的需求。</p>

社区 > 代码,开源社区砥砺前行的背后故事

<p>2019 年,开源云原生 API 网关&nbsp;<a href="https://apisix.apache.org/">Apache APISIX</a>&nbsp;横空出世,在经历了几个版本的代码迭代后,由 API7.ai 捐赠给了&nbsp;<a href="https://www.apache.org/">Apache 软件基金会</a>。截止到目前,APISIX 已经成长为 API 网关领域炙手可热的选择之一,在全球拥有多个知名的企业用户,涉及多个行业领域。从国内用户熟悉的爱奇艺、vivo、腾讯云等,到国外的 NASA、Airwallex 等。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/06/rlk72eJx_1.png" alt="部分企业用户" referrerpolicy="no-referrer"></p> <p>当然,除了企业用户给予了非常多的肯定外,在全球范围的用户使用中,APISIX 在开源层面也呈现了非常优秀的数据。目前 APISIX 项目在 GitHub 上已得到 10K+ star,并拥有 6 亿多下载量和超过 500 人的开源贡献者们。<strong>项目的快速成长与发展,除了产品本身的功能进步外,也离不开产品背后的社区与人。</strong> 在使用 APISIX 的用户和企业中,他们反复提到过“APISIX 社区的响应速度真的超级快”。所以从这个角度来看,APISIX 快速增长的秘诀还是得益于整个社区的氛围,更具体地说是它的活跃性和开放性。而这也正是 Apache Way 的践行标准之一,以开放、透明的姿态去呈现产品和与人沟通。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/06/QALZ2FXB_2.png" alt="Apache APISIX 吸引超 500 名贡献者" referrerpolicy="no-referrer"></p> <h2>APISIX 社区的活跃离不开 API7.ai 的努力</h2> <p>如果你曾在 APISIX 社区互动过,这个互动不只是说在 GitHub 上提个 issue 或者是在 Slack 频道里提问问题,也包括在国内的一些平台上,APISIX 社区的小伙伴们都表现出了及时的响应速度与问答的友好性。</p> <p>这种活跃度很大程度上是来自于 APISIX 对于用户们需求的解决,在使用 APISIX 的过程中,越来越多的用户和企业发现,APISIX 帮助他们解决了很实际的云原生问题。这就会让使用者们在使用之余,也开始回馈社区与更多的用户,从而在后续过程中参与到社区和项目中来。</p> <p>或许除了产品优秀和社区氛围良好的属性外,APISIX 持续活跃运作的背后还包含了 API7.ai 整个公司的努力。</p> <p>API7.ai 的创始人们在将 APISIX 项目捐赠给 Apache 软件基金会后,仍然在社区背后默默支持与维护着整个项目与社区。除了功能迭代上的加持外,API7.ai 的员工在全球范围内定期举办线下 Meetup 或者线上的网络研讨会,让全球各地的小伙伴都能了解和使用上 APISIX。</p> <p>在即将过去的 2022 年,API7.ai 的员工参与主持和组织了近 40 场面对面的线下活动与在线分享。比如周期性的 APISIX Ingress 社区会议、APISIX 社区会议、行业案例分享以及与其他社区合办的线下活动等。当然,今年在海外也进行了非常多的活动与会议,比如下图中展示的是 APISIX 社区在加拿大多伦多举办的线下 Meetup。通过这些在线活动,全球各地的朋友都可以参与到 APISIX 的分享盛宴中来。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/06/CkbUBisY_3.jpg" alt="活动吸引了 40+ 参与者到场" referrerpolicy="no-referrer"></p> <p>除了线下及线上一些小范围的活动外,由 API7.ai 主办的&nbsp;<a href="https://apisix-summit.org/">Apache APISIX Summit Asia 2022</a>&nbsp;则是最为正式以及范围最广的全球性社区活动了。从效果来看,Apache APISIX Summit Asia 2022 成为帮助组织和企业更好地理解和使用开源 API 管理工具的又一次全新尝试。</p> <p><img src="https://static.apiseven.com/uploads/2023/02/06/0DDL6THZ_4.png" alt="Apache APISIX Summit Asia 2022 " referrerpolicy="no-referrer"></p> <p>在为期两天的活动中,我们见证了不同行业领域下的 APISIX 实践用例。由来自腾讯云、阿里云、VMvare Tanzu Labs 等顶级科技公司的工程师以及 APISIX PMC 成员们,带来了干货满满又富有借鉴意义的技术展示。除此之外,还有许多深入但有趣的圆桌研讨会,众多的开源贡献者和投资者就构建社区优先开源软件的原因和方法分享了他们的想法。</p> <h2>多元化多样性的人文故事</h2> <p>除了积极组织和参与活动外,API7.ai 的同事们也在助力让 APISIX 在全世界各地被看到。</p> <p>比如积极推进全球化的开源计划&nbsp;<a href="https://summerofcode.withgoogle.com/">Google Summer of Code (GSoC)</a>,让全球各地的学生们参与开源并开始接触 APISIX;在国内,也有为国内学生们打造的开源之旅——<a href="https://www.devstream.io/zh/community/ospp-2022/summary/">开源之夏</a>,APISIX 项目也已连续两年积极参与其中。并通过这些项目的产出,让学生们对开源的兴趣大增,为后续开源年轻势力输送了超多的新鲜血液。</p> <p><strong>除此之外,社区温馨的环境也吸引了不同属性的朋友来参与到 APISIX 中来,使得 APISIX 拥有一个由不同年龄、种族、性别和经验的人组成的多元化社区,他们在各个领域展示着不同的角色力量。</strong></p> <p>从 2020 年开始,泽平(<a href="https://github.com/bzp2010">bzp2010</a>)&nbsp;一直在积极参与 APISIX 项目的贡献。当时的他还是一名管理学专业的大三学生,正在为他的个人项目寻找合适的 API 管理工具。在偶然机会下,他使用了开源项目 APISIX 并得到了不错的使用反馈,有使用问题也会及时在社区内进行询问,基本都可以在半天时间内得到响应。这点也成为吸引他进入 APISIX 社区并让他继续为 APISIX 社区做出更多贡献、并成为 APISIX PMC 成员的重要因素。“如果你去翻一翻&nbsp;<a href="https://apisix.apache.org/zh/team/">APISIX 社区的 PMC 成员和 Comitter 列表</a>,你会发现有很多像我这样年轻的学生或者应届毕业生。”忘了说,泽平还是目前年龄最小的 APISIX PMC 成员。</p> <p>与泽平一样,泽轩(<a href="https://github.com/spacewander">spacewander</a>)&nbsp;也在持续为 APISIX 项目做贡献。作为 OpenResty 的核心开发者,他已经写了 7 年多的 Go &amp; Lua,因此对于 APISIX 基于 Lua 开发的框架来说,他上手起来更是信手拈来。他在 2019 年发现了 Apache APISIX 社区,带着自己扎实的技术基础他为 APISIX 的产品提供了非常多的功能贡献,并最终成为 APISIX PMC 成员和 API7.ai 的技术主管。</p> <p><a href="https://github.com/SylviaBABY">Sylvia</a>&nbsp;的故事与他们有所不同。如果你平时有关注到 APISIX 官网的博客文章,或许早已看过她经手的文章。Sylvia 是一名接触开源 4 年的开源爱好者,之前也在其他社区中进行过相关贡献,现如今她为 APISIX 的文档和博客提供了很多帮助。“参与过几次线下 Meetup,遇到了很多社区中其他的女性朋友,也感受到了她们为社区活动付出的努力。”事实上,APISIX 社区不仅重视平等机会,也在致力让更多的女性参与到社区中来。比如本月 APISIX 与聚焦科技、风投和商业领域的女性社区&nbsp;Amelia&nbsp;合作举办了在线聚会,讨论了女性及其在开源软件领域的成功。</p> <h2>未来……</h2> <p>Apache APISIX 社区的故事还在继续,文中提到的这些故事也只是数千名真正的 APISIX 爱好者中的一小部分,但他们也为我们展示了为什么 APISIX 在过去 3 年中获得了如此多的关注。</p> <p>从 2019 年开始,APISIX 从 0 成长了很多。作为 APISIX 开源社区的背后商业公司,API7.ai 也一直在见证着这份作品的进步与荣誉。今年早些时候,APISIX 被 CSDN 机构评为评为“年度云原生技术”,并被 Stackshare 评为“ 2021 年 100+ 开发者工具 ”。通过努力践行者 “<strong>社区&gt;代码</strong> ” 的理念,APISIX 社区将继续在开源层面进行各种更有意义的对话。随着 2023 年的临近,API7.ai 的所有人都真诚地期待看到 APISIX 被越来越多的人发现、使用,并提出更多有实践意义的建议。</p>

AWS ACM 如何与 Kubernetes 配合使用?

<h2>为什么需要 AWS ACM</h2> <h3>手动管理证书带来的问题</h3> <p>证书包含持有者、颁发者、失效日期等信息。为了确保证书能够被安全地使用,同时也确保证书能够被及时吊销和续订,证书的相关信息需要被记录下来,例如可以手动记录到电子表格中。但随着管理的证书越来越多,手动管理的方式容易因遗漏而导致证书中断,它无法根据新信息和法规自动更新。因为证书到期之前电子表格不会通知您,也不会自动为即将到期的证书续订。手动管理的方式不可避免会操作失误,这将为您带来不必要的风险。这时,自动管理证书的方式应运而生,它将减少您的工作负担和手工操作带来的风险。</p> <h3>ACM 自动管理证书</h3> <p>AWS Certificate Manager(ACM) 可以轻松预置、管理、部署和续订 SSL/TLS 证书。您可以直接通过 ACM 签发证书保护您的 AWS 网站和应用程序,或者将第三方证书导入 ACM 管理系统中。使用 ACM,您无需经历过去与使用和管理 SSL/TLS 证书相关的大量手动流程。您还可以导出由 AWS Private CA 签名的 ACM 证书,以便在内部 PKI 中的任何位置使用。同时,还集成了 AWS Elastic Load Balancing(ELB)。</p> <p><img src="https://raw.githubusercontent.com/AlinsRan/rd-docs/main/images/aws-acm.jpg" alt="acm" referrerpolicy="no-referrer"></p> <h2>ACM Private CA 解决了哪些问题</h2> <p>当组织中没有任何 Private CA,在部署内部应用程序时,他们都使用了自签名证书。这些应用程序相互访问时,由于对方的不受信任而拒绝访问,如果盲目信任未知来源的应用程序会带来安全风险。这就需要有一个 Private CA 的托管服务,负责创建和管理 CA 层次结构,确保组织内的应用程序之间是受信任的。</p> <p>ACM Private CA 就是一种高可用的托管服务,用于为您的组织创建和维护内部公钥基础设施,消除了持续维护的成本。私钥存储在经过 FIPS 140-2 认证的 AWS 托管硬件安全模块 (HSM) 中。与 Kubernetes 中的默认 CA 相比,提供更安全的证书颁发机构解决方案。</p> <p><img src="https://d2908q01vomqb2.cloudfront.net/22d200f8670dbdb3e253a90eee5098477c95c23d/2021/07/13/TLS-enabled-Kubernetes-clusters-2.png" alt="pca" referrerpolicy="no-referrer"></p> <h2>如何与 Kubernetes 配合使用</h2> <p>在 Kubernetes 中终止 TLS 证书有两种配置方式:</p> <p><img src="https://raw.githubusercontent.com/AlinsRan/rd-docs/main/images/tls-method-2.jpg" alt="配置方式" referrerpolicy="no-referrer"></p> <ul> <li><strong>在 NLB 终止</strong>:对于一些使用公开信任的证书,在 NLB 级别终止 TLS 证书是最常见的用例。此方式易于配置,将 ACM 公开信任证书绑定到 NLB。集群内部进行应用的访问依然用的是 HTTP 访问方式,无需额外的加解密运算。</li> <li><strong>在 Ingress 终止</strong>: 当应用之间有加密需求,需要在 Ingress controller 中终止 TLS 。每个应用都可以通过 Ingress 管理自己的证书,互不干扰,能够保证应用之间的通信是受信任的,此方式更易于配置和管理。</li> </ul> <p>在后续的演示示例中,您可以在 Amazon EKS 上设置 APISIX Ingress,我们将会根据这两种方式演示 APISIX Ingress 如何与 ACM 配合使用(以及 ACM Private CA)。</p> <h2>预置环境</h2> <p>开始之前,您需要具备以下条件:</p> <ul> <li><a href="https://signin.aws.amazon.com/signin?redirect_uri=https://portal.aws.amazon.com/billing/signup/resume&amp;client_id=signup">AWS</a> 账号与 AWS 命令行界面<a href="http://aws.amazon.com/cli">(AWS CLI)</a>.</li> <li>您必须有权使用 Amazon EKS IAM 角色和服务相关角色、<a href="http://aws.amazon.com/cloudformation">AWS CloudFormation</a>、<a href="http://aws.amazon.com/vpc">Amazon Virtual Private Cloud (Amazon VPC)</a>和相关资源。请参阅 IAM 用户指南中的<a href="https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonelastickubernetesservice.html">适用于 Kubernetes 的 Amazon Elastic Container Service 的操作、资源和条件键</a>以及使用<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html">服务相关角色</a>。此外,此 IAM 安全主体需要附加一个 AWSCertificateManagerPrivateCAFullAccess IAM 托管策略。</li> <li>安装并配置了 <strong>kubectl</strong> 和 <a href="https://docs.aws.amazon.com/zh_en/eks/latest/userguide/getting-started-eksctl.html">eksctl</a> 工具。</li> </ul> <h3>Amazon EKS 集群</h3> <p>Amazon EKS 是一种托管服务,您可以使用它在 AWS 上运行 Kubernetes,轻松地部署和管理您的 Kubernetes 集群。本文将使用 eksctl 命令工具来管理集群。</p> <ul> <li>使用 eksctl 默认配置创建集群(如果您已有 EKS 集群请忽略)</li> </ul> <pre><code class="language-bash">eksctl create cluster </code></pre> <h3>安装 APISIX Ingress</h3> <ol> <li>在 EKS 集群中安装 APISIX Ingress,并设置为 LoadBalancer 类型。</li> </ol> <pre><code class="language-bash">helm repo add apisix https://charts.apiseven.com helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update helm install apisix apisix/apisix \ --set gateway.type=LoadBalancer \ --set gateway.tls.enabled=true \ --set ingress-controller.enabled=true \ --namespace ingress-apisix \ --create-namespace </code></pre> <p>:::note</p> <p>请确保您的集群能够添加持久性卷,详情请参阅 <a href="https://docs.aws.amazon.com/zh_en/eks/latest/userguide/storage.html">Amazon EKS Storage</a>。 如果只是为了体验本教程,需要在安装时配置 <code>--set etcd.persistence.enabled=false</code> 声明不使用持久性卷。</p> <p>:::</p> <ol start="2"> <li>运行以下命令检查状态,确保全部 Pod 处于 Running。</li> </ol> <pre><code class="language-shell">$ kubectl get pods -n ingress-apisix NAME READY STATUS RESTARTS AGE apisix-78bfc58588-qspmm 1/1 Running 0 103s apisix-etcd-0 1/1 Running 0 103s apisix-etcd-1 1/1 Running 0 103s apisix-etcd-2 1/1 Running 0 103s apisix-ingress-controller-6ff56cd4b4-rktr9 1/1 Running 0 103s </code></pre> <ol start="3"> <li>检查 NLB 状态,这里重点关注 PORT(S) 和 EXTERNAL-IP。</li> </ol> <pre><code class="language-shell">$ kubectl get svc apisix-gateway -n ingress-apisix NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE apisix-gateway LoadBalancer 10.100.178.65 a6cffe9f6fc5c47b9929cb758610fc5a-2074689558.ap-northeast-1.elb.amazonaws.com 80:30851/TCP,443:32735/TCP 103s </code></pre> <h2>在 NLB 终止 TLS 证书</h2> <h3>准备 ACM 证书</h3> <ol> <li>打开 <a href="https://console.aws.amazon.com/acm/home">ACM 控制台</a>,为你的自定义域申请公共 ACM 证书或导入自定义证书</li> </ol> <p><img src="https://raw.githubusercontent.com/AlinsRan/rd-docs/main/images/acm-1.jpg" alt="acm" referrerpolicy="no-referrer"></p> <h3>LoadBalancer 配置证书</h3> <ol> <li>打开 <a href="https://console.aws.amazon.com/ec2/">EC2 控制台</a>,选择您的 <strong>Load Balancers(负载均衡器)</strong> -&gt; <strong>侦听器</strong> -&gt; <strong>edit</strong>。</li> </ol> <p><img src="https://raw.githubusercontent.com/AlinsRan/rd-docs/main/images/nlb-1.jpg" alt="nlb-1" referrerpolicy="no-referrer"></p> <ol start="2"> <li>NLB 协议设置为 HTTPS,端口 443,实例协议设置为 HTTP,端口 30851。</li> </ol> <p><img src="https://raw.githubusercontent.com/AlinsRan/rd-docs/main/images/nlb-2.jpeg" alt="nlb-2" referrerpolicy="no-referrer"></p> <ol start="3"> <li>将 ACM 中的 TLS 证书附加到 NLB。</li> </ol> <p><img src="https://raw.githubusercontent.com/AlinsRan/rd-docs/main/images/nlb-3.jpg" alt="nlb-3" referrerpolicy="no-referrer"></p> <h3>将自定义域与负载均衡器名称关联</h3> <p>您可以使用 DNS 提供商的控制台,通过 CNAME 将应用程序的 DNS 记录指向 NLB 的 URL。例如 <a href="https://console.aws.amazon.com/route53">Route53</a>,<a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/using-domain-names-with-elb.html#dns-associate-custom-elb">并设置指向您的 NLB 的 CNAME 记录</a>。</p> <pre><code class="language-text">httpbin.example-test.org CNAME a6cffe9f6fc5c47b9929cb758610fc5a-2074689558.ap-northeast-1.elb.amazonaws.com </code></pre> <h3>访问应用域名</h3> <pre><code class="language-bash">curl https://httpbin.example-test.org </code></pre> <h2>在 Ingress 终止 TLS 证书</h2> <p><em><strong>在开始本示例前,请确保 AWS NLB 的配置已恢复如下图片所示:</strong></em></p> <p><img src="https://raw.githubusercontent.com/AlinsRan/rd-docs/main/images/nlb-0.jpeg" alt="nls-0" referrerpolicy="no-referrer"></p> <h3>安装 cert-manager</h3> <p>cert-manager 是一个 Kubernetes 附加组件,可用于自动管理和颁发来自各种颁发来源的 TLS 证书,您可以在常规方式<a href="https://cert-manager.io/docs/installation/helm/">安装 cert-manager</a>。</p> <h3>创建 ACM Private CA</h3> <ol> <li>打开 ACM PCA 控制台,选择创建证书,并安装</li> </ol> <p><img src="https://raw.githubusercontent.com/AlinsRan/rd-docs/main/images/pca-1.jpeg" alt="pca-1" referrerpolicy="no-referrer"></p> <ol start="2"> <li>在 CA 成功创建后,状态为 active,其中 PCA 的 <strong>ARN</strong> 将会在后续流程中多次使用。</li> </ol> <p><img src="https://raw.githubusercontent.com/AlinsRan/rd-docs/main/images/pca-2.jpeg" alt="pca-2" referrerpolicy="no-referrer"></p> <h3>为 ACM Private CA 设置 EKS 节点权限</h3> <p>默认情况下是无颁发权限,为了从 ACM Private CA 颁发证书,需要将 IAM 策略添加到 EKS NodeInstanceRole 中,包括 iamserviceaccount 服务角色。</p> <ol> <li>创建 pca-iam-policy.json 文件,需要将 <code>${PCA_ARN}</code> 替换成您的 PCA_ARN</li> </ol> <pre><code class="language-json">{ "Version": "2012-10-17", "Statement": [ { "Sid": "awspcaissuer", "Action": [ "acm-pca:DescribeCertificateAuthority", "acm-pca:GetCertificate", "acm-pca:IssueCertificate" ], "Effect": "Allow", "Resource": "${PCA_ARN}" } ] } </code></pre> <ol start="2"> <li>根据 pca-iam-policy.json 创建 IAM 和 iamserviceaccount,需要将 <code>${account_id}</code> 替换为您的 Amazon 账户ID。</li> </ol> <pre><code class="language-bash">aws iam create-policy \ --policy-name AWSPCAIssuerIAMPolicy \ --policy-document file://pca-iam-policy.json # 创建命名空间 kubectl create namespace aws-pca-issuer eksctl create iamserviceaccount \ --cluster=${cluster_name} \ --namespace=aws-pca-issuer \ --name=aws-pca-issuer \ --attach-policy-arn=arn:aws:iam::${account_id}:policy/AWSPCAIssuerIAMPolicy \ --override-existing-serviceaccounts \ --approve </code></pre> <h3>安装 aws-privateca-issuer</h3> <p>AWS PrivateCA Issuer 充当 cert-manager 的插件(<a href="https://cert-manager.io/docs/configuration/external/">External Issuers</a>)签署证书请求。</p> <ol> <li>使用 Helm 安装</li> </ol> <pre><code class="language-bash">helm repo add awspca https://cert-manager.github.io/aws-privateca-issuer helm repo update helm install aws-pca-issuer awspca/aws-privateca-issuer \ -n aws-pca-issuer \ --set serviceAccount.create=false \ --set serviceAccount.name=aws-pca-issuer </code></pre> <ol start="2"> <li>查看状态</li> </ol> <pre><code class="language-shell">$ kubectl get pods -n aws-pca-issuer NAME READY STATUS RESTARTS AGE aws-pca-issuer-aws-privateca-issuer-5cdd4b4687-z52n7 1/1 Running 0 20s </code></pre> <h3>创建颁发者并申请证书</h3> <ol> <li>将 issuer-cert.yaml 文件中的 <code>${PCA_ARN}</code> 替换成您的配置,并执行如下命令:</li> </ol> <ul> <li><code>kubectl apply -f issuer-cert.yaml</code></li> </ul> <pre><code class="language-yaml"># issuer-cert.yaml apiVersion: awspca.cert-manager.io/v1beta1 kind: AWSPCAClusterIssuer metadata: name: demo-test-root-ca spec: arn: ${PCA_ARN} --- kind: Certificate apiVersion: cert-manager.io/v1 metadata: name: nlb-lab-tls-cert spec: commonName: httpbin.example-test.org # 需要替换成您的自定义域名 dnsNames: - httpbin.example-test.org # 需要替换为您的自定义域名 duration: 2160h0m0s issuerRef: group: awspca.cert-manager.io kind: AWSPCAClusterIssuer name: demo-test-root-ca renewBefore: 360h0m0s secretName: nlb-tls-app-secret usages: - server auth - client auth privateKey: algorithm: "RSA" size: 2048 </code></pre> <ol start="2"> <li>运行以下命令验证证书是否已颁发,secret 是否已生成。</li> </ol> <pre><code class="language-shell">$ kubectl get cert NAME READY SECRET AGE nlb-lab-tls-cert True nlb-tls-app-secret 10s $ kubectl get secret NAME TYPE DATA AGE nlb-tls-app-secret kubernetes.io/tls 3 8s </code></pre> <h3>公开并保护 httpbin 应用程序</h3> <p><a href="https://www.apiseven.com/blog/using-apisix-ingress-with-aws-acm#%E5%AE%89%E8%A3%85-apisix-ingress"><em><strong>请确保 APISIX Ingress 已成功安装</strong></em></a></p> <ol> <li>部署 httpbin 应用程序</li> </ol> <pre><code class="language-bash">kubectl run httpbin --image kennethreitz/httpbin --port 80 kubectl expose pod httpbin --port 80 </code></pre> <ol start="2"> <li>创建 Ingress 以公开并保护 httpbin 应用程序</li> </ol> <ul> <li><code>kubectl apply -f ingress-httpbin.yaml</code></li> </ul> <pre><code class="language-yaml"># ingress-httpbin.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: httpbin-demo-apisix spec: ingressClassName: apisix tls: - hosts: - httpbin.example-test.org secretName: nlb-tls-app-secret rules: - host: httpbin.example-test.org http: paths: - backend: service: name: httpbin port: number: 80 path: / pathType: Prefix </code></pre> <ol start="3"> <li>访问应用域名</li> </ol> <p><a href="https://www.apiseven.com/blog/using-apisix-ingress-with-aws-acm#%E5%B0%86%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9F%9F%E4%B8%8E%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E5%99%A8%E5%90%8D%E7%A7%B0%E5%85%B3%E8%81%94">请确保您的自定义域已与负载均衡器名称关联</a>。</p> <pre><code class="language-shell">$ curl https://httpbin.example-test.org/headers --cacert acm-pca/cacert.pem { "headers": { "Accept": "*/*", "Host": "httpbin.example-test.org", "User-Agent": "curl/7.74.0", "X-Forwarded-Host": "httpbin.example-test.org" } } </code></pre> <h2>总结</h2> <p>本文中通过实践演示了 APISIX Ingress 与 AWS ACM 和 ACM Private CA 组件配合使用,并介绍了在 Kubernetes 中终止 TLS 证书的两种配置方式。在公开信任的证书中,ACM + NLB 的配置方式会更合适。如果应用之间有加密的要求,ACM Private CA 提供了安全性更强的托管服务。希望本文的实践环节能帮助读者在 AWS EKS 集群中更有效配置和管理 TLS 流量。</p>

盘点微服务架构下的诸多身份认证方式

<h2>什么是身份认证服务</h2> <p>身份认证是授予用户访问系统并授予使用系统的必要权限的过程。而提供了这一功能的服务,就是身份认证服务。在传统的单体软件应用程序中,所有这些都发生在同一个应用程序中,但在微服务架构中,系统由多个服务组成。在这样的架构中,每个微服务都有自己的任务,因此为每个微服务分别实现授权和身份验证过程并不完全符合此原则。本文会先比较传统服务架构中的身份认证和微服务架构下的身份认证,显示出架构上的变化所带来的身份认证服务的变化,并在最后衡量微服务架构中身份认证服务的各种实现的优劣。</p> <h2>传统服务架构中的身份认证服务</h2> <p>在企业开发服务的早期,所有功能都是做到同一个应用程序里面的。我们把这种模式称之为 “单体”,以跟当下更为主流的 “微服务” 架构区分开来。</p> <p>单体应用由单个不可分割的单元组成。它通常由各个业务线各自开发,但是部署时放入到同一个环境中。所有这些都紧密集成以在一个单元中提供所有功能。这一单元里拥有所需的所有资源。单体应用的好处在于部署迭代简单,适合业务线较少且比较独立的公司采用。</p> <p>随着企业开发出来的业务越来越复杂,我们会发现单体服务已经无法满足现实生活里面快速迭代的需要了。我们需要把这个单体的巨无霸拆分一下,同时保证现有的各个功能间的调用能正常进行。这时候,ESB(企业服务总线)便应运而生了。</p> <p>所谓的 “企业服务总线”,就是一根连接各个企业服务的管道。ESB 的存在是为了集成基于不同协议的不同服务,ESB 做了消息的转化、解释以及路由的工作,以此来让不同的服务互联互通。从名称就能知道,它的概念借鉴了计算机组成原理中的通信模型 —— 总线,所有需要和外部系统通信的系统,统统接入 ESB,就可以利用现有的系统构建一个全新的松耦合的异构的分布式系统。</p> <p>ESB 做了消息的转换解释与路由等工作,让不同的服务互联互通。传统的 ESB 的服务调用方式是,每一次服务的调用者要向服务提供者进行服务交互请求时都必须通过中心的 ESB 来进行路由。</p> <p>接下来,我会分别按照这两种情况,讲讲对应的身份认证功能的实现。</p> <h3>单体架构</h3> <p>用户身份验证和会话管理相对简单:身份认证和授权发生在同一个应用程序中,通常使用基于 session 的认证方案,一旦通过身份验证,就会创建一个会话并将其存储在服务器上,任何需要它的组件都可以访问它并用于通知和授权后续要求。会话 ID 被发送到客户端并用于应用程序的所有后续请求,以将请求与当前会话相关联。</p> <h3>ESB 架构</h3> <p>在 ESB 架构下,所有用户与服务之间,服务与服务之间全部通过 ESB 总线进行处理。由于 ESB 的架构是从单体拆分下来的,身份认证方式相对于单体架构并没有变化。</p> <h2>微服务架构中的身份认证服务</h2> <p>从单体架构迁移到微服务架构有很多优势,但微服务架构作为一种分布式架构,会有更大的攻击面,共享用户上下文更加困难。因此微服务架构下需要有跟传统架构不一样的身份认证服务,才能响应更大的安全上的挑战。</p> <p>我们可以把微服务架构下的身份认证服务分为以下三类:</p> <ol> <li>通过每个微服务实现身份认证</li> <li>通过身份认证服务实现身份认证</li> <li>通过网关实现身份认证</li> </ol> <p>每种做法都有自己特定的优缺点。</p> <h3>通过每个微服务实现身份认证</h3> <p><img src="https://static.apiseven.com/2022/12/13/63981cb275c0e.png" alt="per service authentication" referrerpolicy="no-referrer"></p> <p>既然微服务架构是从单体架构拆分出来的,一种自然的过渡方式,就是由每个微服务自己实现身份认证。</p> <p>每个微服务都需要实现自己独立的安全性保障,并在每个入口点上强制执行。此方法使微服务团队能够自主决定如何实现其安全解决方案。但是,这种方法有几个缺点:</p> <ul> <li>安全逻辑需要在每个微服务中重复实现。这会导致服务之间的代码重复。</li> <li>它分散了开发团队的注意力,使其无法专注于其主要服务。</li> <li>每个微服务都依赖于它不拥有的用户身份验证数据。</li> <li>很难维护和监控。</li> </ul> <p>完善这个解决方案的一个选择是使用一个加载在每个微服务上的共享认证库。这将防止代码重复,开发团队将只关注他们的业务领域。但是,仍然存在这种改进无法解决的缺点。因为共享的认证库仍然需要有对应的用户身份数据,而且还需要保证各个微服务使用同样版本的认证库。老实说,共享认证库更像是服务拆分不透彻的结果。</p> <p>优势:实施速度快,独立性强 劣势:服务之间的代码重复;违反单一职责原则;难维护</p> <h3>通过身份认证服务实现身份认证</h3> <p><img src="https://static.apiseven.com/2022/12/13/63981cb296588.png" alt="authentication in a service" referrerpolicy="no-referrer"></p> <p>既然每个微服务自己实现身份认证难以维护,而使用共享认证库又违背了微服务拆分的本意,那么能不能把共享认证库升级成专门的身份认证服务呢?</p> <p>在这种情况下,所有访问都通过同一服务进行控制,类似于单体应用里面的身份认证功能。每个业务服务都必须在执行操作时,向访问控制模块发送单独的授权请求。</p> <p>但是,这种方法在一定程度上减慢了服务的运行速度,并增加了服务之间的互连量。并且各个微服务会依赖这个“单点”的身份认证服务。万一统一的身份认证服务出问题,会造成链式反应,带来二次伤害。</p> <p>优势:每个微服务职责单一,身份认证集中 劣势:造成单点依赖;增加请求延迟</p> <h3>通过网关实现身份认证</h3> <p><img src="https://static.apiseven.com/2022/12/13/63981cbde67af.png" alt="authentication in the API gateway" referrerpolicy="no-referrer"></p> <p>迁移到微服务体系结构时,需要回答的问题之一是微服务之间如何通信。前面提到的 ESB 是种方案,但是更常见的是,采用 API 网关。API 网关是所有请求的单个终端节点入口。它通过充当使用这些微服务的中央接口来提供灵活性。某个需要访问其他微服务的微服务(以下称之为“客户端”,以跟被它访问的微服务相区分)不是有权访问多个服务,而是向负责将其路由到上游服务的 API 网关发送请求。</p> <p>由于 API 网关位于客户端访问的必经之路上,因此它是强制实施身份验证问题的绝佳候选项。它减少了延迟(调用身份验证服务),并确保身份验证过程在整个应用程序中保持一致。</p> <p>举个例子,通过 APISIX 的 <a href="https://apisix.apache.org/docs/apisix/plugins/jwt-auth/">jwt-auth</a> 插件,我们可以在网关上实现身份认证。</p> <p>首先,我们需要规划若干个用户身份信息(名称、密钥等等),并配置到 APISIX 上。 其次,根据给定的用户的密钥,向 APISIX 发起签名请求,得到这个用户的 JWT token。 接着,当客户端需要访问某个上游服务时,带上 JWT token,由 APISIX 作为 API 网关代理这个访问。 最后,APISIX 会通过 JWT token,完成身份认证的操作。</p> <p>当然,凡事有利就有弊,没有完全无劣势的技术选型。使用网关来完成身份认证,还是带来了少许单点问题。比起在每个微服务内完成身份认证,在网关上解决该问题,安全性相比会降低些。比如 API 网关被攻破之后,就可以访问该网关背后的任何微服务。但是风险是相对的,比起统一的身份认证服务,使用 API 网关的单点问题并没有那么严重。</p> <p>优势:有效保护后端微服务;微服务不用处理任何认证逻辑 劣势:还是有少许的单点依赖</p> <h2>总结</h2> <p>在不同的场景下,我们会需要不同的身份认证方案。在单体应用中,身份认证发生在同一个应用程序中,服务端保存了所有的会话。进入微服务时代,单体应用演变为分布式服务,单体应用中的身份认证方式在微服务中并不适用。在微服务架构中,我们有三种身份认证的方式可供选择:</p> <ol> <li>通过每个微服务实现身份认证</li> <li>通过身份认证服务实现身份认证</li> <li>通过网关实现身份认证</li> </ol> <p>每种选择都有属于自己的利弊,需要根据具体的实际情况做具体分析。</p>

API 网关策略的二三事

<p>近些年随着云原生、微服务架构的日趋发展,API 网关以流量入口的角色在技术架构中扮演着越来越重要的作用。API 网关主要负责接收所有请求的流量并进行处理转发至上游服务,API 网关的策略决定了 API 网关处理这些流量的逻辑与规则,直接决定了实际的业务流量行为。</p> <h2>什么是 API 网关策略</h2> <p>API 网关一般位于所有的上游服务之前,当用户向服务发送请求后请求会先到 API 网关,API 网关接收到请求之后一般会判断几件事情:</p> <ol> <li>请求是否合法,比如是否来自被禁止访问的用户列表中;</li> <li>这个请求是否通过认证,访问的内容是否是经过授权的;</li> <li>请求是否触发了某些限制规则,比如限流限速等;</li> <li>请求应该转发给哪个上游服务;</li> </ol> <p>经过这一系列步骤这个请求要么不符合预设的规则被拒绝,要么经过了层层处理正确到达指定的上游服务中。我们将这些处理规则称之为 API 网关的策略。这些规则由网关的管理员在网关运行时不断添加至网关中,网关接受这些规则并根据这些规则作出正确的流量处理行为。</p> <p>以 API 网关 <a href="https://apisix.apache.org/">Apache APISIX</a> 为例,APISIX 的配置信息有两种:网关启动用的配置文件,比如 <a href="https://github.com/apache/apisix/blob/master/conf/config.yaml"><code>config.yaml</code></a>,这个文件决定了网关正常启动所必须的一些配置。另外在运行时管理员可以通过 <a href="https://apisix.apache.org/docs/apisix/admin-api/">Admin API</a> 动态创建各种规则与配置,比如 Route、Consumer、Plugin 等等。API 网关的策略就是管理员通过 Admin API 动态创建的各种规则与配置。</p> <p>本文不再额外描述基本常用场景,而是针对认证授权、安全、流量处理与可观测性这四类 API 网关中重要的场景进行阐述,介绍每种场景下包含的一些 API 网关策略的作用以及使用方法。</p> <h2>认证和授权策略</h2> <p>认证可以确认 API 调用者的身份,授权主要限制调用者仅能访问权限内的资源。</p> <p><img src="https://static.apiseven.com/2022/11/30/63872080f2eb4.png" alt="Authentication and Authorization" referrerpolicy="no-referrer"></p> <p>比如说一位乘客前往车站出行,进入车站之前会使用身份证进行“认证”确认身份,在进入车站后出示车票,经工作人员确认后被“授权”进入某班列车。</p> <p>认证授权类策略主要目的是保证网关转发到上游服务的所有请求都是经过认证和授权的,不会出现非法请求。并且访问的都是权限范围内的资源。比较常用的策略有下面几种:</p> <h3>Basic Auth</h3> <p><a href="https://en.wikipedia.org/wiki/Basic_access_authentication">基本访问认证</a> 策略,这是一种最简单的访问控制技术。一般由用户的 HTTP 代理在发出请求时携带用于认证的请求头,一般为:<code>Authorization: Basic &lt;credentials&gt;</code>,credentials 中即包含了用户认证需要的用户 ID 和密码,使用 <code>:</code> 隔离。这种方式不需要登陆页面、cookie 等繁杂的设置,仅仅基于请求头中的简单凭据信息进行认证,一般为用户名和密码,配置使用起来较为简单。</p> <p>携带基本认证的 <code>cURL</code> 请求的示例如下,用户名为 <code>username</code>,密码为 <code>password</code>:</p> <pre><code>curl -i -u 'username:password' http://127.0.0.1:8080/hello </code></pre> <p>需要注意的是 <code>credentials</code> 中的信息在传输过程中并不会被加密,仅仅做 base64 编码,所以通常需要和 HTTPS 一起使用来保证密码的安全性。</p> <p>在网关中实施这一策略后,未携带凭据的请求将无法正常通过网关转发,除非在请求中携带了正确的认证信息,实现了最小成本下为 API 添加了访问验证。</p> <h3>Key Auth</h3> <p>Key Auth 策略通过在 API 中添加 key 来限制 API 调用,并识别请求携带的 key 来进行访问资源的控制。只有携带了正确的 key 之后的请求才能正常访问,可以在请求头中或 Query 中携带。通常还可以通过这个 key 来区分不同的 API 调用方,从而可以针对不同的调用方实施不同的其他策略或资源控制。同样的 key 在 HTTP 中是明文传输的,确保请求使用了 HTTPS 以保证安全。</p> <p>以 APISIX 的 <a href="https://apisix.apache.org/docs/apisix/plugins/key-auth/">key-auth</a> 插件为例,插件需要通过 Admin API 创建一个具有唯一 key 值的 Consumer:</p> <pre><code>curl http://127.0.0.1:9180/apisix/admin/consumers \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "jack", "plugins": { "key-auth": { "key": "jack-key" } } }' </code></pre> <p>这一请求创建了一个名字为 <code>jack</code> 的 Consumer,它的 key 值为 <code>jack-key</code>。</p> <p>在路由中启用插件时需要配置网关从请求中读取 key 值的位置和字段名称。默认的配置位置为 <code>header</code> ,字段名称为 <code>apikey</code>, 那么正确的请求这个路由的示例为:</p> <pre><code>curl -i http://127.0.0.1:8080/hello -H 'apikey: jack-key' </code></pre> <p>APISIX 在收到这一请求后会从请求中解析出 key,然后从配置的所有 key 中找到和这个请求匹配的 Consumer <code>jack</code>,这样网关就清楚这个请求是 <code>jack</code> 发出的。如果没有找到匹配的 key 即可判定为非法请求。</p> <h3>JSON Web Token</h3> <p>JSON Web Token (JWT) 是一个开放的<a href="https://www.rfc-editor.org/rfc/rfc7519">标准</a>,它定义了一种以 json 对象形式在各方之间安全传递信息的方式。JWT 策略可以集认证和授权于一身,在用户取得授权后会向用户传输一个 jwt token,在后面的所有请求中调用方都会携带这个 token 从而保证请求是被授权的。</p> <p>在 API 网关中可以通过 jwt 策略将 <a href="https://jwt.io/">JWT</a> 身份验证能力添加到网关中,从而把这层逻辑从业务中抽离出来,业务实现者可以更加专注实现业务逻辑。以 APISIX 的 <a href="https://apisix.apache.org/zh/docs/apisix/plugins/jwt-auth/">jwt-auth</a> 插件为例,插件需要在 Consumer 中启用并配置唯一的 key、加密用的公私钥、加密算法、token 过期时间等。同时还需要在路由中启用这一插件并配置网关读取 token 的位置和字段,比如 header、query、cookie 等。该插件会在 API 网关中添加一个 API 用于签发 token。在发送请求之前需要请求签发 token 的 API 获得 token,发送请求时需要根据配置信息在指定的位置上携带这一 token。在请求到达网关后网关会按照配置信息从请求的指定位置读取 token 并验证 token 的有效性,验证通过后该请求才能被转发至上游服务。</p> <p>相较于前两种策略,jwt 策略包含了过期时间选项,签发的 token 随着时间流逝是可以过期的,但是 Basic Auth 和 Key Auth 的有效期是永久的,除非服务端更换了密码或 key。除此之外 jwt 策略可以在多个服务之间公用,尤其是针对单点登录场景下很有用。</p> <h3>OpenID Connect</h3> <p><a href="https://openid.net/connect/">OpenID Connect</a> 是建立在 OAuth2.0 协议之上的身份认证方法,为应用的授权提供了比较完整的方案,API 网关中的 OpenID Connect 策略将允许上游服务从身份提供者(IdP)中获取请求中的用户信息,从而保护 API 安全。常见的 IdP 有 Keycloak、Ory Hydra、Okta、Auth0 等等。以 Apache APISIX 为例网关中的 <a href="https://apisix.apache.org/docs/apisix/plugins/openid-connect/">OpenID Connect</a> 策略工作流程如下:</p> <p><a href="https://apisix.apache.org/blog/2022/07/06/use-keycloak-with-api-gateway-to-secure-apis/"><img src="https://static.apiseven.com/apisix-webp/2022/blog/0706/2.webp" alt="OpenID Connect Work Flow" referrerpolicy="no-referrer"></a></p> <ol> <li>客户端向网关发出请求</li> <li>网关收到请求后向 IdP 发出认证请求</li> <li>用户将被重定向到 IdP 提供的页面完成登陆认证</li> <li>IdP 重定向到网关并携带认证 code</li> <li>网关通过 code 向 IdP 请求 Access Token 从而获取用户信息</li> <li>网关向上游转发请求时即可携带用户信息</li> </ol> <p>通过这一流程可以将认证和鉴权从业务中独立出来放置于网关中解决,使架构更加清晰。</p> <p>关于更多 APISIX 的认证授权方法可以参考 <a href="https://api7.ai/blog/api-gateway-authentication">API Gateway Authentication</a>。</p> <h2>安全策略</h2> <p>API 网关安全策略像门卫一样保证 API 安全访问,允许正常请求被网关转发并在网关上拦截非法请求。根据 <a href="https://owasp.org/www-project-api-security/">OWASP API Security Project</a>,在 API 的调用者中存在着大量可能的威胁和攻击。使用 API 网关安全策略可以对所有的 API 请求进行安全验证,在 API 免于遭受这些安全威胁上起到了重要作用。</p> <p><img src="https://static.apiseven.com/2022/12/01/63885e303c63a.png" alt="API security" referrerpolicy="no-referrer"></p> <p>下面是几种比较重要的 API 网关安全策略:</p> <h3>IP 访问限制</h3> <p>IP 限制策略通过将某些 IP 或 <a href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing">CIDR</a> 设置为白名单或者黑名单来限制某些客户端对 API 的访问,确保重要数据不会被随意访问。正确配置这一策略很大程度上提高了 API 的安全性,实现了更高的 API 安全治理。</p> <h3>URI 拦截</h3> <p>URI 拦截策略主要通过设置 URI 的一些规则来阻止潜在的危险 API 请求。比如一些安全攻击通过嗅探 URI 路径从而发现潜在的安全漏洞进而攻击。Apache APISIX 提供了 <a href="https://apisix.apache.org/docs/apisix/plugins/uri-blocker/"><code>uri-blocker</code></a> 插件来提供这一能力。通过 <code>uri-blocker</code> 插件可以设置一些正则规则,如果请求命中规则就可以拦截当前用户的请求,例如设置 <code>root.exe</code>、<code>admin</code> ,这一插件就可以阻止 <code>*/root.exe</code> 和 <code>*/admin</code> 这种类似的请求,进一步保护 API 安全。</p> <h3>CORS</h3> <p>CORS 即浏览器针对跨域请求作出的安全策略。一般情况下在浏览器中发出 xhr 请求前浏览器会验证请求地址和当前地址是否为<a href="https://developer.mozilla.org/en-US/docs/Glossary/Origin">同一域</a>,如果在同一域下请求可以直接发出,否则浏览器会先出发一个 OPTION 类型的跨域预检请求,然后在该请求的响应头中会有 CORS 相关的设置,例如允许跨域请求的方法、允许请求携带的凭据等。浏览器会根据这些信息决定是否发出正式的请求,详细可以参考 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS</a>。</p> <p>一般情况下包含 CORS 设置的响应是由后端服务设置的,但是如果服务数量很多,在网关层面针对不同服务统一处理会更加便捷安全。CORS 策略可以在不同的 API 上设置不同的跨域解决策略,上游服务无需再处理这些逻辑。</p> <h3>CSRF</h3> <p><a href="https://owasp.org/www-community/attacks/csrf">CSRF</a> 即跨站请求伪造攻击,通常情况下会使终端用户在他们已经认证的站点中执行非自愿的动作。这种攻击通常伴随着社会工程学(通过电子邮件向攻击者发送攻击链接),当用户点击这一链接后利用攻击者在网站中已登陆认证的身份执行攻击操作。在网站看来因为用户已经登陆,所以所做的任何操作都是正常的。</p> <p>通常网站的后端服务需要添加额外的中间件处理这部分逻辑,防范的方法也都有统一的标准。使用 CSRF 策略可以为网关提供防范这一攻击的能力,在网关层统一做 CSRF 安全处理,简化上游服务逻辑复杂度。</p> <h2>流量处理策略</h2> <p>流量处理策略主要保证 API 网关进行流量转发的上游负载都在健康范围内,同时在请求转发前或者返回给调用者前对请求进行按需改写。这一类型的策略主要围绕限流限速、熔断、缓存、重写等功能展开。</p> <h3>限流限速</h3> <p>在资源有限的情况下,API 可以提供的服务能力是有一定限度的,如果调用超过了这一限制可能会使上游服务崩溃继而引起一些连锁反应。限流限速可以防范这种情况的发生,另一方面也可以有效防止 API 遭受 DDOS 攻击。</p> <p>在限流限速策略中可以设置一个时间窗口和可允许最大的请求数量,在时间窗口内超过这个数量的请求会被拒绝并返回设置的信息,直到请求数量低于设定的值或到下一个时间窗口后会允许继续访问。</p> <p>请求计数的凭据可以设置为请求中的变量或着某一个请求头等,例如根据不同的 IP 设置相应的限速策略。实现更加灵活的控制。</p> <h3>熔断</h3> <p>API 熔断策略可以为上游服务提供熔断能力,使用这一策略时需要设置上游服务健康和不健康的状态码,用于网关判断上游服务状态。另外还需要设置触发熔断或者恢复健康的请求次数,连续达到这一次数后即判定为对应的状态。当上游服务连续向网关返回一定次数的不健康状态码后,网关就会熔断该上游服务一段时间,在这段时间内不再向该上游转发请求而是由网关直接返回错误。可以防止上游服务因为错误后继续接收请求出现 “雪崩”,保护业务服务。超过这一时间后网关会再次尝试向上游转发请求,如果还是返回不健康的状态码,网关就会继续熔断更长的时间(加倍)。直到转发请求后上游连续返回了一定次数的健康状态码,则网关认为上游服务恢复健康,后续会继续往该上游节点转发流量。</p> <p>在这个策略中还需要设置当不健康后需要返回的状态码和信息,当上游服务不健康后请求在网关层面直接返回,保护业务服务稳定。</p> <h3>流量拆分</h3> <p>流量拆分策略可以动态控制将流量按比例转发给不同的上游服务,这在<a href="https://en.wikipedia.org/wiki/Feature_toggle#Canary_release">灰度发布</a>或<a href="https://en.wikipedia.org/wiki/Blue-green_deployment">蓝绿发布</a>中非常有用。</p> <p>灰度发布又名金丝雀发布,当服务发布新功能时可以仅让一部分请求使用新的服务,另一部分仍然停留在旧的服务中。如果新服务保持稳定,则可以增加比例逐步将所有请求转移到新的服务中,直至比例完全切换,完成服务升级。</p> <p>蓝绿发布是另一种发布模式,可以做到在用户使用的高峰期进行发布,同时不会中断服务。服务的旧版本和新版本会同时共存。一般会将生产环境(蓝色)复制到一个相同但是单独的容器中(绿色)。在绿色环境中发布新的更新,之后将绿色和蓝色一同发布至生产环境。之后就可以在绿色环境中进行测试和修复,在这期间用户访问的还是蓝色系统。之后可以使用某些负载均衡策略将请求重定向到绿色环境中。蓝色环境即可保持待机作为灾难恢复选项或者用作下一次更新。</p> <p>APISIX 通过 <a href="https://apisix.apache.org/docs/apisix/plugins/traffic-split/">traffic-split</a> 插件通过流量拆分对这两种发布都做出了很好的支持,使得业务部署更加便捷可靠。</p> <h3>请求重写</h3> <p>在现代微服务架构中,尤其是服务端与服务、服务与服务之间充斥各种不同的协议,或着请求数据格式不统一,这些问题如果单独在各自服务之间实现转换处理会产生很多冗余的逻辑代码并且难以管理。一些请求重写策略可以处理一些协议转换、请求体改写等逻辑。</p> <p>APISIX 提供了 <a href="https://apisix.apache.org/docs/apisix/plugins/response-rewrite/">Response Rewrite</a> 插件可以用来修改上游服务返回的 Body 或者 Header 信息内容,支持添加或者删除响应头,设置规则修改响应体等。这在设置 CORS 响应头实现跨域请求设置或者设置 Location 实现重定向等场景中很有用。</p> <p>另一方面,对于请求重写 APISIX 则提供了 <a href="https://apisix.apache.org/docs/apisix/plugins/proxy-rewrite/">proxy-rewrite</a> 插件也可以处理代理到上游服务的请求内容,可以对请求的 URI、方法、请求头等重写,在很多场景下为业务处理提供了便捷。</p> <h3>故障注入</h3> <p>故障注入测试是一种软件测试方法,通过在系统中故意引入错误来确保系统的行为正常。通常在部署之前进行测试以保证在生产环境中没有潜在的故障。在一些混沌测试场景下,需要对服务注入一些错误来验证服务的可靠性。</p> <p>软件的故障注入可以分为编译时注入和运行时注入。编译时注入指在编写软件的过程中通过改变某些代码或逻辑来实现;运行时注入通过向正在运行的软件环境中设置错误来测试软件的行为。故障注入策略可以在网关中以运行时注入的方式,模拟应用网络请求中的故障。通过在策略中设置一个比例,命中这个比例内的请求会执行设置好的故障逻辑,比如延迟时间返回,或直接返回设置的错误码和错误信息给调用方。通过这种方式可以增加软件的适应性,让开发人员提前看到可能出现的一些错误情况,在发布之前针对问题做出适应性修改。</p> <h3>协议转换</h3> <p>协议转换类的策略可以做一些常见协议之间的转换。比如常见的 HTTP 请求和 gRPC 之间的转换。Apache APISIX 提供了 <a href="https://apisix.apache.org/zh/docs/apisix/plugins/grpc-transcode/">grpc-transcode</a> 插件可以实现在网关接收到 HTTP 请求之后,将请求转码并转发给 gRPC 类型的服务,接收到响应后以 HTTP 的格式返回给客户端。这样客户端无需关注上游 gRPC 的类型,只处理 HTTP 即可。</p> <h2>可观测性策略</h2> <p>可观测性指在一个系统中通过系统的输出数据来衡量这个系统运行状态的能力。在一些简单的系统中,因为系统组件数量相对较少,出现问题时可以通过分析各个组件状态得到答案。但是在大型分布式系统中,各种微服务组件数量非常大,对组件一一进行排查显然是不现实的,这个时候就需要系统具备可观测性。可观测性提供了对分布式系统的“可见性”,当系统出现问题时它可以提供工程师所需的控制能力,准确定位问题。</p> <p><img src="https://static.apiseven.com/2022/12/01/638875bb7112a.png" alt="API Gateway Observability policy" referrerpolicy="no-referrer"></p> <p>可观测性的数据收集可以在应用程序组件内实现,也可以在其他位置实现。API 网关作为所有流量的入口,在 API 网关中实现系统的可观测性,可以清晰反映出系统 API 的使用情况。API 网关的可观测性策略可以帮助到公司的每个团队:</p> <ul> <li>工程师团队可以监控并解决 API 问题;</li> <li>产品团队可以了解 API 的使用情况以挖掘背后的商业价值;</li> <li>销售和增长团队可以监控 API 使用情况,观察商业机会并确保 API 提供正确的数据。</li> </ul> <p>可观测性策略根据输出的数据类型一般分为三类:Tracing,Metrics 和 Logging。</p> <h3>Tracing</h3> <p>在大型分布式系统中服务之间的调用关系错综复杂,Tracing(链路追踪)可以在分布式应用中追踪完整的调用链路、应用之间的依赖分析以及请求统计等内容。在系统出现问题时可以帮助工程师确定排查范围和位置。</p> <p>Tracing 策略可以在 API 网关上集成一些其他的分布式调用链路追踪系统,收集信息并记录。常见的服务比如 Zipkin、SkyWalking 等。通过 Tracing 策略将这些服务集成到 API 网关中,实现在网关上数据收集和与这些服务之间的通信,可以帮助工程师解决诸如这个请求接触了什么服务以及引入了多少延迟等问题。Tracing 策略使工程师能够进一步确认在特定的会话或相关的 API 调用中要看哪些日志,确认排查范围。</p> <h3>Metrics</h3> <p>Metrics 指在服务运行期间收集到的一个时间间隔内软件自己的各种观测数据,这些数据默认是结构化的,可以更好地实现查询和可视化。通过对这些数据分析可以掌握系统当下的运行状态和行为。</p> <p>Metrics 策略可以在 API 网关中集成 Prometheus 或 Datadog 这一类服务,为系统提供监控、报警等能力。这一策略通过 API 网关中的各种接口收集网关运行过程中的数据,并将数据上报至上述服务中。通过将这些数据可视化后开发者可以清晰看到网关的运行状态,API 请求的统计信息等数据统计图。</p> <h3>Logging</h3> <p>日志是在某个特定时间系统事件的文本记录,当系统出现问题时日志是首要排查的地方。当服务出现一些意外情况时工程师依赖日志内容查看系统“发生了什么”从而找出对应的解决方法。日志内容一般分为两类:API 请求日志和网关自身的运行日志。API 请求日志记录了 API 网关在运行期间所有的 API 请求记录,通过这些记录工程师可以掌握 API 访问情况,及时发现并排查异常请求。网关自身的运行日志则包含了网关在工作期间发生的所有事件的记录,当 API 网关自身出现异常时可以作为排查问题的重要依据。</p> <p>Logging 策略可以将 API 网关中的日志存储在服务器磁盘中或是推送到一些其他的日志服务器中,比如 HTTP 日志服务器、TCP 日志服务器、UDP 日志服务器等,或者是其他的日志系统比如 Kafka、RocketMQ、Clickhouse 等。</p> <h2>总结</h2> <p>这篇文章介绍了什么是 API 网关策略,并针对认证授权、安全、流量处理与可观测性这四类 API 网关中常用的策略进行描述。API 网关在所有上游服务之前接收请求的流量,控制一个请求是否要转发以及如何进行转发,对不安全的、未授权的请求直接拒绝或进行限制,这些行为都可以由 API 网关策略决定。</p>

微服务架构下服务网格的出现带来了什么?

<h2>服务网格介绍</h2> <p>服务网格是一种技术架构,它用于管理微服务系统中各个服务之间的通信,旨在处理微服务间的流量(也称为东西向流量)。</p> <p>在云原生应用中,一个应用的背后可能存在着成百上千个服务,各个服务可能又有着若干个实例,各个实例的状态也一直在变化。在如此复杂的服务运行环境中,如何保障用户的可靠访问以及维持业务的平稳运行成为一个很大的挑战,服务网格的治理方案便应运而生。</p> <p>服务网格就像是微服务间的 TCP/IP,负责服务间的网络调用、限流限速、监控等。服务网格目前主要应用在 Kubernetes 平台上,其最经典的一种实现模式是 Sidecar 模式:将一些通用功能抽象到 Sidecar 容器中,并将 Sidecar 容器与服务容器挂载在同一个 Pod 里。由于 Sidecar 容器与服务容器并行,且各个 Sidecar 间相互通讯,共同构成了网格形式的网络,因此称之为服务网格。如下图所示:</p> <div align="center"> <img width="70%" alt="Service Mesh architecture" src="https://static.apiseven.com/2022/11/25/63802d4665194.jpg" referrerpolicy="no-referrer"> </div> <p>Sidecar 并非唯一的一种服务网格实现模式,除此之外还有 DaemonSet 模式及 Ambient mesh模式:</p> <ul> <li> <p>DaemonSet 模式与 Sidecar 的不同在于它仅在 Kubernetes 集群的每个节点上运行一个,这个 Pod 承担了类似 Sidecar 代理的工作。相比于 Sidecar 模式,DaemonSet 模式占用的机器资源会更少,当然它也会有其他的缺点,例如隔离性差,资源调度难预测等,更多差异点可以参考这篇文章:<a href="https://wecode.wepay.com/posts/scds-battle-of-containerization">Sidecars and DaemonSets: Battle of containerization patterns</a></p> </li> <li> <p>Ambient mesh 模式是 Istio 于 2022 年 9 月 7 日宣布的一种全新的数据面模式,它将数据面的代理从应用 pod 中剥离出来独立部署,以解决 mesh 基础设施和应用部署耦合的问题。Ambient mesh 将数据面分为安全覆盖层和七层处理层:安全覆盖层负责 TCP 路由、监控指标、访问日志、mTLS 隧道等功能;七层处理层则除了安全覆盖层的功能外,提供 HTTP 协议的流量管控,可观测性等功能,并且可以实现丰富的七层授权策略。另外 Ambient mesh 还使用了一个共享代理 ztunnel(零信任隧道),它运行在 Kubernetes 集群的每个节点上,负责安全地连接与认证 mesh 内的工作负载。关于 Ambient mesh 模式更多细节可以看参考这个文档:<a href="https://istio.io/latest/blog/2022/introducing-ambient-mesh/">Introducing Ambient Mesh</a></p> </li> </ul> <h2>为什么我们需要服务网格</h2> <p>在服务网格流行之前,很多微服务架构的服务治理都是通过微服务框架配合控制平台实现的,这种方式存在以下几个问题:</p> <ol> <li>框架与业务耦合,整体复杂度与运维难度很高,且开发者需要对公共库有一定的了解,没法只专注于业务实现。</li> <li>需要维护多种语言版本的框架,增加了维护的成本。</li> <li>微服务框架本身的升级成本比较高,在升级时往往需要进行业务重启等操作。</li> <li>线上存在很多版本的框架,会导致复杂的兼容性考虑。</li> </ol> <p>面对以上的这几个问题,原 Twitter 工程师威廉 · 摩根 (Willian Morgan),Linkerd 的创建者之一, 提出了 “Service Mesh” 的概念,即服务网格。服务网格通过 Sidecar 模式实现在不侵入业务服务的情况下将基础设施与业务逻辑解耦,实现跨语言统一更新发布及运维。</p> <p><img src="https://static.apiseven.com/2022/12/01/6388968b87fef.jpg" alt="Microservices Framework to Service Mesh" referrerpolicy="no-referrer"></p> <p>服务网格将流量管理,可观测性,安全通讯等功能下沉到基础组件,因此开发者无需关心通信层和服务治理的具体实现,与通信相关的一切工作直接交给服务网格,让开发者能够更关注于业务的开发。基于服务网格的这些特点,前面提到的几个问题都能够得到有效解决。</p> <h2>服务网格是如何工作的</h2> <p>服务网格不会为应用的运行时环境加入新功能,任何架构中的应用还是需要相应的规则来指定请求如何从 A 点到达 B 点。但服务网格的不同之处在于,它从各个服务中提取逻辑管理的服务间通信,并将其抽象为一个基础架构层。</p> <p>目前服务网格大多数采用是数据面 + 控制面的架构模式,如下图所示:</p> <div align="center"> <img width="70%" alt="Data plane &amp; Control plane" src="https://static.apiseven.com/2022/11/25/63802dd4eafb2.jpg" referrerpolicy="no-referrer"> </div> <h3>控制面</h3> <p>控制面用于管理和配置数据面以及在运行时执行策略。单个网格中控制平面的所有实例共享相同的配置资源。</p> <p>控制面聚焦于安全、可观测性、流量管控等策略的处理和下发,同时还能够收集和集中数据平面的遥测数据,供运维人员使用。</p> <h3>数据面</h3> <p>数据面通常以代理的形式实现,是由一个个的网络代理 Sidecar 组成,Sidecar 与业务应用实例并行,通过拦截业务数据流以管控业务应用的流量。</p> <p>在前面的介绍中有提到服务网格是将 Sidecar 设计模式在 Kubernetes 进行实现,通过容器化的方式实现了封装,Sidecar 主张以额外的容器来扩展或增强主容器,这个额外的容器被称为 Sidecar 容器,它与业务容器在同一个 Pod 中。而服务网格即是一个个 Sidecar 代理所构成的网格式网络。</p> <h2>服务网格的实际应用</h2> <p>在微服务架构中,工程师往往会为对外暴露的服务采取加密或访问限制的措施以保障服务的安全,但却忽视了集群内部的流量通信安全,所以至今仍有很多微服务应用没有采取服务间通信的加密措施,集群内部的流量以明文的形式进行传输,非常容易导致内部流量遭到数据窃听或是中间人攻击。</p> <p>而为了防止集群内部流量遭到攻击,通常会使用 mTLS 将通讯数据进行加密。mTLS 可以用于确保服务网格中微服务之间的通信安全。它使用加密安全技术相互认证各个微服务并加密它们之间的流量。</p> <div align="center"> <img width="70%" alt="mTLS Comparison" src="https://static.apiseven.com/2022/12/01/63888cca79e1f.jpg" referrerpolicy="no-referrer"> </div> <p>虽然可以直接在微服务中定义通信安全策略并执行身份验证和加密,但在每一个微服务中去单独实现相同的功能效率是很低的。而且增加功能还需要改动业务代码,侵入业务逻辑。且即便完成了功能,后期的升级迭代与测试都需要开发者投入额外精力去维护,无法专注于业务功能的开发。</p> <p>而使用服务网格,我们就可以在不影响原本业务的情况下零感知的为服务提供 mTLS 通信。因为在服务网格中,服务通信相关的功能都被转移至 Sidecar 代理中实现。</p> <p>当两个微服务需要通信时,将由 Sidecar 建立 mTLS 连接,加密的流量将通过该 mTLS 连接流动。Sidecar 之间会交换证书并通过证书颁发机构相互认证。在发起连接前, Sidecar 会检查控制面推送的配置中的授权策略,以判断是否允许微服务进行通信。如果允许则 Sidecar 会使用生成的会话密钥建立安全链接,加密微服务间的通信数据。<strong>在整个实现过程中,业务应用都不会受到影响,降低开发者心智负担。</strong></p> <div align="center"> <img width="80%" alt="mTLS" src="https://static.apiseven.com/2022/12/01/6388932e2bbbf.jpg" referrerpolicy="no-referrer"> </div> <p>通过这个场景,相信大家就能理解服务网格是如何做到在不影响业务的情况下拓展当前应用的功能的。当然,服务网格除了可以实现类似 mTLS 这类的内部流量安全配置功能,通过调整控制面的配置还能快速的拓展包括流量管控,可观测性,协议编解码等更多功能。</p> <h2>总结</h2> <p>这篇文章为大家介绍了服务网格基础概念、工作原理以及服务网格可以为我们带来的价值。服务网格为微服务架构带来了很大的变革,让开发者从复杂的微服务环境中抽身,专注于业务功能的开发。</p> <p>虽然服务网格解决了很多微服务架构中的痛点,但它也同时有自己的局限性,在软件开发中复杂度是不灭的,只是在不同的部分之间做转移。我们将服务治理抽离为单独的一层就要面对流量链路的增长以及运维难度的提升,且服务网格需要在云原生的环境中使用,这对于运维的专业能力及工程实践经验有了更高的要求。所以说技术只是用于解决问题的工具,服务网格能带来的价值还是得从应用的从实际情况出发。</p> <p>随着云原生的爆炸式发展及服务网格的不断优化,未来的服务网格可能会完全取代传统微服务架构,成为各个企业微服务及云原生改造的首选架构。</p>

为什么 APISIX Ingress 是比 Ingress NGINX 更好的选择?

<p>Kubernetes 中的服务可以通过 Ingress 暴露出来,流量路由由 Ingress 资源上定义的规则控制,通常需要 Ingress controller 负责实现。</p> <p>本文将会对比两个比较流行的 Ingress controller 实现,希望能对读者进行 Ingress controller 选型中有所帮助。</p> <ul> <li><a href="https://github.com/kubernetes/ingress-nginx">Ingress NGINX</a> 是 Kubernetes 社区实现的 Ingress controller,在社区中被广泛使用。</li> <li><a href="https://github.com/apache/apisix-ingress-controller">Apache APISIX Ingress</a> 是 Apache 软件基金会下的开源项目,使用 APISIX 作为数据面的 Kubernetes Ingress controller。</li> </ul> <h2>Ingress NGINX vs APISIX Ingress</h2> <h3>功能对比</h3> <p>下列表格中,对比了 Ingress NGINX 和 APISIX Ingress 基本功能,包括协议支持、鉴权方式、上游探针/策略、负载均衡策略、Kubenertes 集成等。以下表格数据取自 <a href="https://docs.google.com/spreadsheets/d/191WWNpjJ2za6-nbG4ZoUMXMpUK8KlCIosvQB0f-oq3k">learnk8s.io</a>。</p> <table> <thead> <tr> <th style="text-align:left">Product/Project</th> <th style="text-align:left"></th> <th style="text-align:left">Ingress NGINX</th> <th style="text-align:left">Apache APISIX Ingress</th> </tr> </thead> <tbody> <tr> <td style="text-align:left">1. General info</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Based on</td> <td style="text-align:left">nginx</td> <td style="text-align:left">nginx</td> </tr> <tr> <td style="text-align:left">2. Protocols</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">HTTP/HTTPS</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">HTTP2</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">gRPC</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">TCP</td> <td style="text-align:left">Partial</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">TCP+TLS</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">UDP</td> <td style="text-align:left">Partial</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Websockets</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Proxy Protocol</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">QUIC/HTTP3</td> <td style="text-align:left">Preview</td> <td style="text-align:left">Preview</td> </tr> <tr> <td style="text-align:left">3. Clients</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Rate limiting (L7)</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">WAF</td> <td style="text-align:left">✔️</td> <td style="text-align:left">Partial</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Timeouts</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Safe-list/Block-list</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Authentication</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Authorisation</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">4. Traffic routing</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Host</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Path</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Headers</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Querystring</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Method</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">ClientIP</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">5. Upstream probes/resiliency</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Healthchecks</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Retries</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Circuit Breaker</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">6.Load balancer strategies</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Round robin</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Sticky sessions</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Least connections</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Ring hash</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Custom load balancing</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">7. Authentication</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Basic auth</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">External Auth</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Client certificate - mTLS</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">OAuth</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">OpenID</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">JWT</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">LDAP</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">HMAC</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">8. Observability</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Logging</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Metrics</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Tracing</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">9. Kubernetes Integration</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">State</td> <td style="text-align:left">Kubernetes</td> <td style="text-align:left">Kubernetes</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">CRD</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Scope</td> <td style="text-align:left">Clusterwide<br>namespace</td> <td style="text-align:left">namespace</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Support for the Gateway API</td> <td style="text-align:left">✖︎</td> <td style="text-align:left">Preview</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Integrates with service meshes</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">10. Traffic shaping</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Canary</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Session Affinity</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Traffic Mirroring</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">11. Other</td> <td style="text-align:left"></td> <td style="text-align:left"></td> <td style="text-align:left"></td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Hot reloading</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">LetsEncrypt Integration</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Wildcard certificate support</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Configure hot reloading</td> <td style="text-align:left">Preview</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left"></td> <td style="text-align:left">Service Discovery</td> <td style="text-align:left">✖</td> <td style="text-align:left">✔️</td> </tr> </tbody> </table> <h3>功能差异</h3> <p>通过下图,可以粗略看到 APISIX Ingress 内置的功能和特性相比 Ingress NGINX 更加丰富,其中包括服务发现、协议支持、认证鉴权等等。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/11/Pg8rLraY_ingress-pk.jpeg" alt="功能差异" referrerpolicy="no-referrer"></p> <h2>服务发现</h2> <p>在微服务架构中,应用被拆分为很多微服务,无论是微服务故障,还是对应用服务进行扩缩容,都需要尽快的通知到调用方,以免调用失败。因此,在微服务架构中,服务注册和发现机制就显得很重要了,通常这会通过注册中心来完成。</p> <table> <thead> <tr> <th style="text-align:left">Service Discovery</th> <th style="text-align:left">Ingress NGINX</th> <th style="text-align:left">Apache APISIX Ingress</th> </tr> </thead> <tbody> <tr> <td style="text-align:left">Kubernetes</td> <td style="text-align:left">✔️</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">DNS</td> <td style="text-align:left">✖</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">nacos</td> <td style="text-align:left">✖</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">exureka</td> <td style="text-align:left">✖</td> <td style="text-align:left">✔️</td> </tr> <tr> <td style="text-align:left">consul_kv</td> <td style="text-align:left">✖</td> <td style="text-align:left">✔️</td> </tr> </tbody> </table> <h2>协议支持</h2> <p>两者都对 HTTP/HTTPS 协议提供完整支持,APISIX Ingress 在协议支持上更丰富一些,能够的使用 TLS 来加密 TCP 流量,还支持 <a href="https://apisix.apache.org/docs/apisix/next/plugins/mqtt-proxy/">MQTT</a>,<a href="https://apisix.apache.org/docs/apisix/next/plugins/dubbo-proxy/">Dubbo</a>、<a href="https://apisix.apache.org/docs/apisix/next/plugins/kafka-proxy/">kafka</a> 等协议进行代理。</p> <h2>完善的服务治理能力</h2> <h3>健康检查</h3> <p>在后端节点故障或者迁移时,不可避免会出现节点不可用的情况。如果大量请求访问到了这些不可用的节点时,将会造成流量损失,导致业务中断。因此,需要对节点进行健康检查,通过探针的形式探测后端节点的可用性,将请求代理到健康的节点,从而减少或避免流量损失。</p> <p>健康检查的能力在 Ingress NGINX 中尚未支持,而 APISIX Ingress 提供了该能力:</p> <ul> <li>主动健康检查:确保后端服务中的 Pod 处于可用的状态。在应用服务进行滚动更新时,会牵扯大量的节点进行更新,不健康的节点将会被负载均衡器忽略,开启健康检查能够有效的挑选出可用的 Pod,避免流量损失。</li> <li>被动健康检查:被动的方式无需发起额外的探针,每个请求就是探针,若一个健康节点连续 N 个请求都被判定为失败(取决于如何配置),则该节点将被标记为不健康。由于无法提前感知节点的状态,可能会有一定量的失败请求,在滚动更新时这种情况会相对常见,可以通过服务降级来避免失败的请求量。</li> </ul> <p>APISIX Ingress 为 httpbin 服务配置健康检查示例:</p> <pre><code class="language-yaml">apiVersion: apisix.apache.org/v2 kind: ApisixUpstream metadata: name: httpbin spec: healthCheck: passive: unhealthy: httpCodes: - 500 httpFailures: 3 active: type: http httpPath: /healthz healthy: successes: 3 interval: 2s httpCodes: - 200 </code></pre> <h3>服务熔断</h3> <p>流量高峰时,网关作为流量入口向后端服务发起调用,后端服务有可能会产生调用失败(超时或者异常),失败时不能让请求堆积在网关上,需要快速失败并返回回去,这就需要在网关上做熔断。</p> <p>服务熔断的功能在 Ingress NGINX 中尚未支持。在 APISIX Ingress 中可以通过 <a href="https://apisix.apache.org/docs/apisix/plugins/api-breaker/">api-breaker</a> 熔断插件来实现。</p> <p>APISIX Ingress 配置服务熔断示例:</p> <pre><code class="language-yaml">apiVersion: apisix.apache.org/v2 kind: ApisixRoute metadata: name: httpbin-route spec: http: - name: rule1 match: hosts: - httpbin.org paths: - /status/* backends: - serviceName: httpbin servicePort: 80 plugins: - name: api-breaker enable: true config: break_response_code: 502 unhealthy: http_statuses: - 505 failures: 2 healthy: http_statuses: - 200 successes: 2 </code></pre> <h2>支持更多的插件和鉴权方式</h2> <p>Ingress NGINX 主要通过 <a href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/">Annotations</a> 和 <a href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/">ConfigMap</a> 等方式进行配置,支持的插件功能比较有限。如果想要使用 JWT、HAMC 等鉴权方式,只能自行开发。</p> <p>APISIX Ingress 原生支持 APISIX 内置的 <a href="https://github.com/apache/apisix/blob/master/conf/config-default.yaml#L391-L473">80+ 插件</a>,功能十分丰富,能够覆盖大部分使用场景,还支持 JWT、HMAC、wolf-rbac 等多种鉴权方式。</p> <p>APISIX Ingress 使用 HMAC 认证并在路由上应用示例:</p> <pre><code class="language-yaml"> apiVersion: apisix.apache.org/v2 kind: ApisixConsumer metadata: name: hmac-value spec: authParameter: hmacAuth: value: access_key: papa secret_key: fatpa algorithm: "hmac-sha256" clock_skew: 0 --- apiVersion: apisix.apache.org/v2 kind: ApisixRoute metadata: name: httpbin-route spec: http: - name: rule1 match: hosts: - httpbin.org paths: - /ip backends: - serviceName: httpbin servicePort: 80 authentication: enable: true type: hmacAuth </code></pre> <h2>Ingress NGINX 和 APISIX Ingress 扩展方式</h2> <p>当 Ingress controller 的基础功能无法满足企业用户的需求时,只能通过扩展的方式进行定制开发。接下来将具体介绍 Ingress NGINX 和 APISIX Ingress 如何进行功能扩展。</p> <h3>Ingress NGINX 如何进行功能扩展</h3> <p>Ingress NGINX 在扩展方式上比较单一,只能通过嵌入 Lua 程序的方式来扩展功能。</p> <p><a href="https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/lua/plugins/README.md">Ingress NGINX 插件开发</a>示例:</p> <ol> <li>编写 Lua 程序 example-plugin</li> <li>将插件安装到 ingress-nginx pod 中的 <code>/etc/nginx/lua/plugins/&lt;your plugin name&gt;</code> $\rightarrow$ <code>/etc/nginx/lua/plugins/example-plugin</code></li> <li>在 ConfigMap 中启用 example-plugin 插件,需要在安装 Ingress NGINX 时引用此 ConfigMap 对象</li> </ol> <pre><code class="language-yaml">apiVersion: v1 kind: ConfigMap metadata: name: ingress-nginx-controller namespace: ingress-nginx data: plugins: "example-plugin" </code></pre> <h3>APISIX Ingress 如何进行功能扩展</h3> <p>APISIX Ingress 提供了多种扩展方式,企业用户可以根据自身情况自由选择、组合,当前支持如下:</p> <ul> <li>通过 <a href="https://apisix.apache.org/docs/apisix/plugin-develop/">Lua 进行插件开发</a>:这种方式相对简单,并且几乎没有性能损耗;</li> <li>通过 *-plugin-runner 开发:这种模式下支持 Java/Python/Go 等语言进行开发,这可以方便用户利用一些现有的业务逻辑,并且无需学习新语言;</li> <li>通过 WASM 进行插件插件:这种模式下,可以使用任何支持构建出 WASM 的语言进行插件开发;</li> </ul> <p>此外还可以通过 serverless plugin 来直接编写 Lua 代码,快速的满足业务需求。</p> <h2>为什么 APISIX Ingress 选择维护 CRD</h2> <p>目前 APISIX Ingress 支持三种声明式配置:Ingress 、CRD 和 Gateway API。这里主要对比 Ingress 和 CRD,Gateway API 将在后续展开。</p> <p>Ingress 比较适合从 Ingress NGINX 迁移的企业用户,其转换成本较低。但缺点也较明显,比如语义化能力弱、没有细致规范等,同时也只能通过 Annotations 方式扩展,且 Annotations 无法支撑复杂配置场景。相对的使用 CRD 主要有以下好处:</p> <ul> <li>更契合数据面的设计语义,更加简单易用;</li> <li>一些重要配置能够被复用,而不会存在冗余庞大的单个配置;</li> <li>功能性和可扩展能力有了巨大提升;</li> <li>数据面 APISIX 有着活跃的社区,更新和发布版本快,CRD 的方式能够轻易支持数据面的更多能力;</li> </ul> <h2>Ingress NGXIN 的痛点:不支持配置热加载</h2> <h3>静态配置带来的问题</h3> <p>Ingress NGINX 主要基于 NGINX 配置文件的方式,尽管使用 NGINX + Lua 来实现功能扩展,但没有彻底解决静态配置文件的问题。在路由能力和加载模式上稍显不足,并且存在一些明显劣势。比如添加、修改任何新的规则时,需要重新加载 NGINX 配置。随着越来越多的路由规则和证书,在触发变更时,reload 操作将会更耗时,甚至需要几秒到十几秒的时间,对线上流量的影响将会非常大的,会导致流量短暂中断、影响响应延迟、负载均衡质量(每次重新加载 NGINX 都会重置负载均衡状态)等。</p> <h3>触发 NGINX 重新加载的情况</h3> <ul> <li>创建新的 Inresss 资源;</li> <li>将TLS 部分添加到现有 Ingress;</li> <li>Ingress Annotations 的变化可能影响上游配置(例如 load-balance 注释不需要重新加载);</li> <li>在 Ingress 中添加或删除 path;</li> <li>Ingress、Service、Secret 资源被删除;</li> <li>Secret 发生更新;</li> </ul> <p>以上这些情况,涵盖了 Ingress controller 大量的使用场景。在具有频繁部署应用程序的集群环境中,会不断触发 Ingress、Secret 等资源的操作(创建、更新、删除等),导致 NGINX 重新加载次数剧增,给生产环境带来了极大的影响。</p> <h3>小结</h3> <p>Ingress NGINX 的架构决定了它必须生成 NGINX 配置然后通过 reload 方式完成配置更新,架构不调整是无法解决这些已知问题。比如路由的实现,APISIX Ingress 则不再依赖 NGINX 配置改为了纯内存结构,通过热更新方式实现动态路由,不再需要重启 NGINX。</p> <h2>云原生新一代网关规范 Gateway API</h2> <h3>Gateway API 优势</h3> <p>Gateway API 相比 Ingress 的功能性更强,旨在通过由许多供应商实现并具有广泛行业支持的富有表现力、可扩展和面向角色的接口来发展 Kubernetes 服务网络。Gateway API 具有如下的优势:</p> <ul> <li> <p>面向角色:Gateway 是由一组 API 资源组成的。不同的 API 资源代表了使用与配置 Kubernetes 网络资源的不同角色;</p> </li> <li> <p>表现力强:Gateway API 的核心功能就包含诸如基于头的匹配、流量加权以及其他在 Ingress 中只能通过各实现者自定义的非标准化 Annotations 等方式实现的功能;</p> </li> <li> <p>可扩展:Gateway API 允许不同资源在不同层级一同使用。这使得能够对 API 结构进行更精细化的控制;</p> </li> </ul> <h4>目前支持情况</h4> <p>Gateway API 作为一种扩展 Kubernetes 服务网络的标准,其 Gateway 资源能够实现作为 Kubernetes API 来管理网关的生命周期,功能十分强大。目前许多 Ingress controller 都在积极支持它,包括 Istio、Kong、Traefik 等。在目前 <a href="https://gateway-api.sigs.k8s.io/implementations/#implementation-status">Gateway API 实现情况</a>中,很遗憾的是,<strong>Ingress NGXIN 尚未计划支持 Gateway API</strong> 。而 APISIX Ingress 已经支持了 Gateway API 的大部分特性:包括 HTTPRoute、TCPRoute、TLSRoute、UDPRoute 等。</p> <h2>总结</h2> <p>经过 APISIX Ingress 与 Ingress NGINX 的完整对比,两者基础功能差异不大,也都具备扩展能力。但在微服务的架构中,APISIX Ingress 对服务治理和服务发现的支持更具优势。</p> <p>总体来看,两款开源软件均非常优秀,Ingress NGINX 主要特点是简单、易接入,但缺点也十分明显,APISIX Ingress 作为后来者解决了 NGINX 不支持热加载的痛点,在扩展能力和功能上相比 Ingress NGINX 也具有很大的优势,从项目发展角度而言,支持 Gateway API 和 CRD 能够扩展和丰富 Ingress controller 基础能力。如果读者正在进行 Ingress controller 选型,倾向于功能丰富和更强的扩展能力,推荐使用 APISIX Ingress 。如果只是刚接触 Ingress controller,没有更多的功能需求,Ingress NGINX 也是一个比较好的选择。</p>

RESTful API 为何成为顶流 API 架构风格?

<p>万物互联的世界充满着各式各样的 API ,如何统筹规范 API 至关重要。<a href="https://en.wikipedia.org/wiki/Representational_state_transfer">RESTful API</a> 是目前最流行的 API 架构风格之一,可以帮助你实现客户端服务端关注点分离,让前后端各自迭代,提升效率。它无状态的特性可以让应用更容易扩展,更容易的实现缓存策略从而提升用户体验和性能。 本文来介绍下什么是 RESTful API 以及我们如何使用它。</p> <h2>什么是 API</h2> <p>先抛开什么是 API 这个话题,我们先来聊一聊我们是如何在生活中传递信息。</p> <p>当你在商店将钱拿给老板告诉他你需要买电池,老板在收到钱后在货架上找到电池后递给你。一个买电池的交易顺利完成。</p> <p>计算机软件则通过 API 来完成信息的传递,先来看维基百科定义:</p> <blockquote> <p>An application programming interface (API) is a way for two or more computer programs to communicate with each other. It is a type of software interface, offering a service to other pieces of software.</p> </blockquote> <p>软件 A 通过 API 向软件 B 发起请求,软件 B 在查询自己资源后将请求的响应内容通过 API 返回 A。</p> <p>软件 A 通过 API 向软件 B 发起请求就好比你跟老板说你需要电池,软件 B 在将数据返回给软件 A 就好比老板找到电池后将电池给你。</p> <p>这一过程软件 B 不需要知道软件 A 为什么要数据,就好比商店老板不会问你买电池的目的。软件 A 也不需要知道 B 软件的数据是怎么找到的,就好比你买电池的时候你也不会问老板电池从哪里进货一样。每个软件之间通过 API 传递信息,各司其事,使得程序世界变得有序可靠。</p> <h2>什么是 RESTful API</h2> <p>Roy Fielding 在他 2000 年的博士论文《建筑风格和基于网络的软件架构设计》中定义了REST(Representational state transfer),REST 架构风格定义了六个指导性约束。一个符合<strong>部分</strong>或<strong>全部</strong>这些约束的系统被松散地称为 RESTful。</p> <br> <p><img src="https://static.apiseven.com/uploads/2023/01/11/Sz7uhON3_restfulapi.png" alt="REATful 架构" referrerpolicy="no-referrer"> (Source: Seobility)</p> <h3>REST 的约束条件</h3> <table> <thead> <tr> <th>约束条件</th> <th>详述</th> <th>优势</th> </tr> </thead> <tbody> <tr> <td>Client–server architecture</td> <td>通过将用户界面问题与数据存储问题分开,提高了跨多个平台的用户界面的可移植性,并通过简化服务器组件提高了可扩展性。</td> <td>1. 客户端、服务端解耦。 <br>2. 提升用户平台跨平台的可移植性。<br>3. 提升服务器端的可拓展性。</td> </tr> <tr> <td>Statelessness</td> <td>客户端的每个请求到服务器必须包含请求所需的所有信息,并且不能利用服务器上存储的任何上下文,会话状态完全保存在客户端。</td> <td>1. 易扩展,无会话依赖,任何服务器可以处理任何请求。<br> 2. 易缓存,提升程序性能。</td> </tr> <tr> <td>Cacheability</td> <td>要求请求响应中的数据被隐式或显式标记为可缓存或不可缓存。如果一个响应是可缓存的,那么客户端缓存就被授予为以后的等效请求重用该响应数据的权利。</td> <td>1. 减少带宽。<br> 2. 减少延迟。<br> 3. 减少服务器负载。<br> 4. 隐藏网络状态。</td> </tr> <tr> <td>Layered system</td> <td>通过约束组件行为允许架构由分层层组成,这样每个组件都不能“看到”超出它们与之交互的直接层。通过将系统知识限制在单个层,降低了整个系统的复杂性并促进了底层独立性。</td> <td>1. 降低整个系统的复杂性。<br> 2. 促进底层的独立性。<br> 3. 可方便的实施负载均衡。<br> 4. 可将业务逻辑和安全策略解耦。</td> </tr> <tr> <td>Code on demand (optional)</td> <td>允许通过下载和执行小程序或脚本形式的代码来扩展客户端功能。</td> <td>1. 提高系统的可扩展性。<br></td> </tr> <tr> <td>Uniform interface</td> <td>主要包含四点:<br> 1. Resource identification in requests.<br>客户能够通过请求中包含的 URI 来识别一个资源,将服务端资源和客户端请求资源解耦。<br>2. Resource manipulation through representations.<br>当客户端拥有一个资源的表示,如 URI,那么就有足够的信息来修改或者删除资源。<br>3. Self-descriptive messages. <br>每个消息都包括足够的信息来告知客户客户端该如何处理该信息。<br>4. Hypermedia as the engine of application state (<a href="https://www.apiseven.com/blog/https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FHATEOAS">HATEOAS</a>).<br>客户端不需要任何额外的编码通过服务端返回的资源链接,就可以使得用户获取所有的资源。</td> <td>1. 简化了整体系统架构。<br>2. 提高了交互的可见性。</td> </tr> </tbody> </table> <h2>Restful API 最佳实践</h2> <p>强调组件间的 <strong>统一接口</strong> 是 REST 架构风格与其他基于网络的风格区分开来的核心特征,基于此特征,在这里梳理最佳实践,以帮助你更好的设计 API。</p> <h3>路径名称避免动词</h3> <p>使用 HTTP 方法来表达资源操作行为,而不是将行为动词定义到路径中。</p> <pre><code>// Good curl -X GET http://httpbin.org/orders // Bad curl -X GET "http://httpbin.org/getOrders" </code></pre> <ul> <li>GET 获取指定 URI 的资源信息, 例如:</li> </ul> <pre><code>// 代表获取当前系统的所有订单信息 curl -X GET http://httpbin.org/orders // 代表获取订单编号为 1 的订单详情信息 curl -X GET http://httpbin.org/orders/1 </code></pre> <ul> <li>POST 通过指定的 URI 创建资源</li> </ul> <pre><code> // 代表创建一个名称为 order 的资源 curl -X POST http://httpbin.org/orders \ -d '{"name": "awesome", region: "A"}' \ </code></pre> <ul> <li>PUT 创建或全量替换指定 URI 上的资源</li> </ul> <pre><code> // 代表将 id 为 1 的 order 进行数据替换 curl -X PUT http://httpbin.org/orders/1 \ -d '{"name": "new awesome", region: "B"}' \ </code></pre> <ul> <li>PATCH 执行一个资源的部分更新</li> </ul> <pre><code> // 代表将 id 为 1 的 order 中的 region 字段进行更改,其他数据保持不变 curl -X PATCH http://httpbin.org/orders/1 \ -d '{region: "B"}' \ </code></pre> <ul> <li>DELETE 通过指定的 URI 移除资源</li> </ul> <pre><code> // 代表将 id 为 1 的 order 删除 curl -X DELETE http://httpbin.org/orders/1 </code></pre> <h3>URI 使用复数形式</h3> <p>如果你要使用单数的形式来表示获取某一类资源,例如:</p> <pre><code>curl -X GET "http://httpbin.org/order" </code></pre> <p>使用单数形式会使用户产生该系统中只有一个 order 的困惑,而是用复数形式理解起来就会顺畅很多。</p> <pre><code>curl -X GET "http://httpbin.org/orders" </code></pre> <h3>善用 HTTP 状态码</h3> <p>HTTP 标准定义个状态码,大致可以分为以下几类:</p> <table> <thead> <tr> <th>状态码</th> <th>含义</th> </tr> </thead> <tbody> <tr> <td>2xx</td> <td>成功,操作被成功接收并处理</td> </tr> <tr> <td>3xx</td> <td>重定向,需要进一步的操作以完成请求</td> </tr> <tr> <td>4xx</td> <td>客户端错误,请求包含语法错误或无法完成请求</td> </tr> <tr> <td>5xx</td> <td>服务器错误,服务器在处理请求的过程中发生了错误</td> </tr> </tbody> </table> <p>使用标准状态码,开发人员可以立即识别问题,可以减少发现不同类型错误的时间。</p> <h3>版本管理</h3> <p>随着业务需求的变更,已经上线的 API 大概率是要对应调整的。如果我们的 API 有第三方在使用,让每一个客户端根据我们 API 的变更而变更显然是不可能的,这个时候就要引入 API 版本管理概念了,即既可以保证历史 API 正常使用,又可以迭代新的 API 以满足新的业务需求。</p> <p>常见的版本控制手段有:</p> <ul> <li>通过请求中路径来做版本控制</li> </ul> <pre><code>// 请求 v1 版本的API curl http://httpbin.org/v1/orders // 请求 v2 版本的API curl http://httpbin.org/v2/orders </code></pre> <ul> <li>通过 Query 参数来做版本控制</li> </ul> <pre><code>// 请求 v1 版本的API curl http://httpbin.org/orders?version=v1 // 请求 v2 版本的API curl http://httpbin.org/v2/orders?version=v2 </code></pre> <ul> <li>通过 Header 来做版本控制</li> </ul> <pre><code>// 请求 v1 版本的API curl http://httpbin.org/orders -H "custom-version: v1" // 请求 v2 版本的API curl http://httpbin.org/orders -H "custom-version: v2" </code></pre> <h2>APISIX 如何助力 RESTful API</h2> <p>Apache APISIX 是一个动态、实时、高性能的 API 网关。它可以在任何 RESTful API 服务前面运行,并使用插件来添加新的服务和扩展其功能,这符合 RESTful 定义中的 <strong>Layered system</strong>。此外,对于一些没有遵循 RESTful API 定义的历史服务, APISIX 也可以帮你在<strong>不改动原有业务代码</strong>的情况下完成接口的转换,使你的接口完成 <strong>Uniform interface</strong> 这一 REST 限制条件,使你的 API 可以更好的遵守 RESTful API 规范。</p> <h3>分层系统:支持业务逻辑和安全逻辑的分割</h3> <p>你可以只用关注业务逻辑的实现,接口的安全逻辑可以交给 APISIX Authentication 类插件处理,例如 <a href="https://apisix.apache.org/docs/apisix/plugins/key-auth">key-auth</a>。APISIX 支持大量的 Authentication 插件,我们以 <a href="https://apisix.apache.org/docs/apisix/plugins/openid-connect/">openid-connet</a>为例如下图所示:</p> <p><img src="https://static.apiseven.com/uploads/2023/01/11/0kUFMse8_oauth-1.png" alt="APISIX openid-connet 插件" referrerpolicy="no-referrer"> 我们可以看到,使用 APISIX(API Gateway) 在我们的业务服务器前面加一层认证逻辑,就可以起到保护上游服务的作用,这种架构模式可以很好的让你的业务逻辑和安全逻辑解耦。</p> <h3>Layered system:多负载均衡协议支持</h3> <p>APISIX 作为 API 网关,可以设立在 客户端 和 服务端之间,完成不同的负载需求。你甚至可以自定义负载均衡的逻辑。</p> <p>支持的负载均衡算法有:</p> <ul> <li><code>roundrobin</code>: Round robin balancing with weights.</li> <li><code>chash</code>: Consistent hash.</li> <li><code>ewma</code>: Pick the node with minimum latency. See <a href="https://www.apiseven.com/blog/https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FEWMA_chart">EWMA Chart</a> for more details.</li> <li><code>least_conn</code>: Picks the node with the lowest value of <code>(active_conn + 1) / weight</code>. Here, an active connection is a connection being used by the request and is similar to the concept in Nginx.</li> <li>user-defined load balancer loaded via <code>require("apisix.balancer.your_balancer")</code></li> </ul> <h3>Uniform interface:使历史 API 更加 RESTful</h3> <p>对于已经存在很久的历史 API,如果没有很好的遵循 RESTful API 准则。你可以在不改造原有 API 逻辑的情况下重新通过 APISIX 来封装新的 API 以满足不同的业务场景。</p> <ul> <li>使用 <a href="https://apisix.apache.org/docs/apisix/plugins/proxy-rewrite/">proxy-rewrite</a> 改写客户端请求</li> </ul> <p>在本文中上方提过我们的路径中不要有动词。</p> <p>例如:历史版本 API 有 <code>/getOrder</code> 接口,我们可以通过 proxy-rewrite 插件来将 API 请求代理到历史 API 上</p> <pre><code>curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "methods": ["GET"], "uri": "/orders", "plugins": { "proxy-rewrite": { "uri": "/getOrder", "scheme": "http", } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:80": 1 } } }' </code></pre> <p>你也可以同样使用该插件进行 API 版本管理上的操作。</p> <ul> <li>使用 <a href="https://apisix.apache.org/docs/apisix/plugins/response-rewrite/">response-rewrite</a> 插件改写服务端响应</li> </ul> <p>当我们的历史 API 存在响应状态码不规范时,我们可以通过 response-rewrite 代理 response 响应从而达到修改响应状态码的目的。</p> <pre><code>curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "methods": ["GET"], "uri": "/orders", "plugins": { "response-rewrite": { "status_code": 201, "body": "{\"code\":\"ok\",\"message\":\"new json body\"}", "vars":[ [ "status","==",200 ] ] } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:80": 1 } } }' </code></pre> <p>例如,这个例子表示将请求 /orders 路径的 API 中响应为 200 的状态的请求修改为 201。</p> <p>APISIX 支持非常丰富的插件,期待你去挖掘更多的玩法。</p> <h2>总结</h2> <p>本文介绍了什么是 API, 什么是 RESTful API 以及其最佳实践。另外,本文也介绍了如何通过 APISIX 来实现将业务逻辑和安全逻辑分离,如何使用 APISIX 在不改动原有业务代码的情况下将历史 API 服务更加 RESTful。希望本文对你理解 RESTful API 有所帮助。</p>

当 Amazon Lambda 遇上 Apache APISIX 可以擦出什么火花?

<h2>使用 Amazon Lambda 时为什么需要 Apache APISIX?</h2> <h3>Serverless 和 Amazon Lambda</h3> <h4>什么是 Serverless?</h4> <p>Serverless 的基础概念是将运行服务所需的基础设施交由云服务提供商管理,以及一些自部署的 Serverless 平台,从而让使用 Serverless 的工程师可以专注于面向客户业务应用层的开发,而不需要在基础设施的构建、管理、扩容等任务上投入过多精力。很多云服务提供商也在推出 Serverless 相关的产品,Amazon Serverless 的核心是名为 <a href="https://aws.amazon.com/lambda">Amazon Lambda</a> 的计算服务。</p> <p>如图 1 所示,和传统的开发、编译、部署运行方式不同,使用 Amazon Serverless 计算服务 Lambda,仅需要上传源文件,选择执行环境并执行,便能得到运行结果。在该过程中,服务器部署, runtime 安装、编译、都由 Amazon Serverless 计算平台管理执行。对工程师来说,只需要维护源代码和 Amazon Serverless 执行环境的相关配置即可。于此相关的技术有BaaS(Backend as a Service,后端即服务),是指我们不再编写和/或管理所有服务端组件,把应用中的各个部分完全外包出去,而 Serverless 是一种新的运行代码的托管环境。</p> <p><img src="https://static.apiseven.com/2022/11/29/6386054bc6c9c.png" alt="Architecture Diagram" referrerpolicy="no-referrer"></p> <center>图1 Server vs Serverless</center> <h4>为什么需要 Serverless</h4> <p>对于开发人员而言,Serverless 可以对程序执行细节进行抽象,让业务开发工程师专注于代码本身;对于成本而言,只需按照使用量付费;对于服务性能而言,可以自动响应任何规模的代码执行请求,可以通过调整函数内存大小优化代码执行时间和响应时间。从如图 1 中的对比也可以看出,基于 Serverless 的开发,对于开发人员来说更友好。</p> <h4>使用 Serverless 时为什么需要一个网关?</h4> <p>Serverless 服务的使用也存在一些问题:比如将函数 URL 硬编码到应用程序中;其次应用程序逻辑的授权和身份验证问题也比较繁琐;再者更新云服务提供商的过程也是一个比较艰巨的工程。网关可以天然的解决上述的问题,通过组合的方式,Serverless可以更好的解决上述问题。图 2 描述的是如何用 Amazon Serverless 的相关服务迅速组装一个简单的 Web Service,网关在授权等问题中发挥重要作用。以 Apache APISIX 为例,它为流行的云服务提供商(AWS、Azure)提供 Serverless 框架支持;可以定义一个路由去启用 Serverless 插件,而不是将函数 URL 硬编码到应用程序中;为开发人员提供了热更新函数 URI 的灵活性,更新不同的 FaaS 云服务提供商也没有什么额外的麻烦;此外,这种方法减轻了应用程序逻辑的授权和身份验证问题。</p> <p><img src="https://static.apiseven.com/2022/11/29/6385ff2ce13c3.png" alt="Architecture Diagram" referrerpolicy="no-referrer"></p> <center>图2 用 Amazon Serverless 的相关服务组装一个 Web Service </center> <h3>Apache APISIX</h3> <p><a href="https://apisix.apache.org/">Apache APISIX</a> 是 Apache 软件基金会下的云原生 API 网关,它兼具动态、实时、高性能等特点,提供了负载均衡、动态上游、灰度发布(金丝雀发布)、服务熔断、身份认证、可观测性等丰富的流量管理功能。我们可以使用 Apache APISIX 来处理传统的南北向流量,也可以处理服务间的东西向流量。同时,它也支持作为 K8s Ingress Controller 来使用。APISIX 通过插件来扩充生态,目前也内置了各类插件,覆盖了 API 网关的各种领域,如认证鉴权、安全、可观测性、流量管理、多协议接入等,当然,也包含很多 Serverless 相关插件。</p> <h3>AWS Lambda 插件</h3> <p><code>aws-lambda</code> 插件用于将 AWS Lambda 作为动态上游集成至 APISIX,从而实现将访问指定 URI 的请求代理到 AWS 云。用户使用该插件终止对已配置 URI 的请求,并代表客户端向 AWS Lambda Gateway URI 发起一个新的请求。</p> <p>这个新请求中携带了之前配置的授权详细信息,包括请求头、请求体和参数(以上参数都是从原始请求中传递的),然后 <code>aws-lambda</code> 插件会将带有响应头、状态码和响应体的响应信息返回给使用 APISIX 发起请求的客户端。该插件支持通过 AWS API key 和 AWS IAM secrets 进行授权。插件细节可参考<a href="https://apisix.apache.org/zh/docs/apisix/plugins/aws-lambda">官方文档</a> 或者 <a href="https://blog.bisakh.com/blog/aws-lambda-apisix">博客</a>。</p> <h3>Apache APISIX 与 Serverless 相关插件总结</h3> <p>除了 Amazon Lambda,Apache APISIX 还支持与 Azure Function、Lua 函数和 Apache OpenWhisk 等 Serverless 相关生态的集成,从而提供相应的 Serverless 插件,具体如下。</p> <center>表 1 Apache APISIX Serverless 相关插件具体信息</center> <table> <thead> <tr> <th>插件名称</th> <th>描述</th> </tr> </thead> <tbody> <tr> <td><a href="https://apisix.apache.org/docs/apisix/plugins/serverless/">serverless</a></td> <td>用户可以通过 Serverless 插件上传自定义的 Lua 脚本,并根据配置中的 phase 来指定代码运行阶段。例如在 access 阶段对请求进行访问控制,在 header filter,body filter 阶段,对响应头或响应体进行修改,或者在 log 阶段打印个性化日志等。另外,由于 Serverless 插件是热加载的,因此我们不需要重新启动 Apache APISIX 便可立即生效。</td> </tr> <tr> <td><a href="https://apisix.apache.org/docs/apisix/plugins/azure-functions/">azure-functions</a></td> <td>用于将 Azure Serverless Function 作为动态上游集成至 APISIX,从而实现将访问指定 URI 的请求代理到 Microsoft Azure 云服务。启用 azure-functions 插件后,该插件会终止对已配置 URI 的请求,并代表客户端向 Azure Functions 发起一个新的请求。该新请求中携带了之前配置的授权详细信息,包括请求头、请求体和参数(以上参数都是从原始请求中传递的)。之后便会通过 azure-functions 插件,将带有响应头、状态码和响应体的信息返回给使用 APISIX 发起请求的客户端。</td> </tr> <tr> <td><a href="https://apisix.apache.org/docs/apisix/plugins/openwhisk/">openwhisk</a></td> <td>用于将开源的分布式无服务器平台 Apache OpenWhisk 作为动态上游集成至 APISIX。启用 openwhisk 插件后,该插件会终止对已配置 URI 的请求,并代表客户端向 OpenWhisk 的 API Host 端点发起一个新的请求,然后 openwhisk 插件会将响应信息返回至客户端。</td> </tr> <tr> <td><a href="https://apisix.apache.org/docs/apisix/plugins/openfunction/">openfunction</a></td> <td>用于将开源的分布式无服务器平台 CNCF OpenFunction 作为动态上游集成至 APISIX。启用 openfunction 插件后,该插件会终止对已配置 URI 的请求,并代表客户端向 OpenFunction 的 function 发起一个新的请求,然后 openfunction 插件会将响应信息返回至客户端。</td> </tr> </tbody> </table> <p><img src="https://static.apiseven.com/2022/12/01/638842425ec60.png" alt="Architecture Diagram" referrerpolicy="no-referrer"></p> <center>图3 Apache APISIX 与 Serverless 相关产品的关联图 </center> <h3>总结</h3> <p>近年来,随着微服务架构的出现,一切都在迁移到云端,不少云服务提供商也在推出 Serverless 相关的产品,基于 Serverless 的开发已经成为一种十分便利的开发模式。 本文首先介绍了 Serverless 是什么,以及为什么需要 Serverless;其次,讲述了一个好的网关在 Serverless 架构下的重要性,而 APISIX 就是这样的一个网关;最后,本文重点介绍了 APISIX 中的 Serverless 类型的插件 aws-lambda,同时列举了 Apache APISIX 其它 Serverless 相关插件。</p>

细数 API 网关 APISIX 的认证与鉴权

<h2>认证鉴权对于 API 网关的重要性</h2> <p>API 网关最核心的功能可以概括为:连接 API 消费者和 API 提供者。</p> <p>实际场景中,除去少部分允许匿名访问的 API,提供者往往都会对消费者有所限制:只有符合条件的消费者才可以对 API 进行访问。其次,提供者对于不同的消费者的访问策略可能并不相同,例如 A、B 消费者都可以访问 <code>/send_mail</code> API,但每分钟的调用频次需要区分计算。</p> <p>从以上两点可以看出在 API 网关层面鉴别和验证 API 消费者的身份至关重要,下面我们将会介绍开源 API 网关 Apache APISIX 如何实现对于消费者的认证以及那些被企业广泛采用的认证方式,然后还会进一步介绍 APISIX 的用户认证体系如何与其他安全特性联动使用,从而进一步提升 API 网关的安全防护能力。</p> <p><img src="https://static.apiseven.com/2022/11/18/6376ec4eb8675.png" alt="api gateway authentication" referrerpolicy="no-referrer"></p> <h2>Apache APISIX 的认证鉴权</h2> <p>传统的 HTTP 代理往往只能基于请求域名、客户端 IP 等粗粒度手段对请求方进行识别,这对于一款 API 网关来说是远远不够的,我们需要有更丰富的认证方式来解决越来越复杂的业务需求。而 APISIX 区分于传统代理的一大优势就是灵活的插件扩展能力,这其中就包括一套用于用户认证的插件集合,这些插件根据实现方式的不同可以分为两大类:</p> <ol> <li>对接外部认证服务,委托其进行认证。</li> </ol> <p><img src="https://static.apiseven.com/2022/11/18/6376ed961ca8a.jpg" alt="external auth service" referrerpolicy="no-referrer"></p> <ol start="2"> <li>网关内部认证,配合 APISIX 设计的 Consumer 对象进行认证。</li> </ol> <p><img src="https://static.apiseven.com/2022/11/18/6376eed1460dc.jpg" alt="internal auth" referrerpolicy="no-referrer"></p> <p>下面将会依次介绍这两种认证方式。</p> <h3>对接外部认证服务</h3> <p>在企业采用 API 网关之前,系统中往往已经部署了独立的认证服务,此时我们要如何将 APISIX 与已有的认证服务进行对接呢?APISIX 提供了这样一系列插件,它们的工作原理就是通过对接各种外部的认证服务,委托它们完成认证。</p> <p>例如,我们可以使用 <code>openid-connect</code> 插件对接任意支持 OIDC 协议的认证服务,下面是一段对接到 keycloak 服务的样例配置:</p> <pre><code class="language-shell">curl http://127.0.0.1:9180/apisix/admin/routes -H "X-Api-Key: your-API-key" -XPOST -d ' { "uri":"/*", "plugins":{ "openid-connect":{ "client_id":"apisix", // keycloak 创建 client 时生成 "client_secret":"d5c42c50-3e71-4bbe-aa9e-31083ab29da4", "discovery":"http://keycloak:8080/auth/realms/apisix_test_realm/.well-known/openid-configuration", // keycloak OpenID Endpoint "scope":"openid profile", "bearer_only":false, "realm":"apisix_test_realm", "introspection_endpoint_auth_method":"client_secret_post", "redirect_uri":"http://127.0.0.1:9080/" } }, "upstream":{ ... } }' </code></pre> <h3>网关内部认证</h3> <h4>Consumer</h4> <p><img src="https://static.apiseven.com/2022/11/18/6376ef129743f.jpg" alt="consumer" referrerpolicy="no-referrer"></p> <p>当来自各个来源的请求到达 API 网关后,网关要如何识别出这些调用方非常关键,Apache APISIX 提出了 Consumer 概念,用来代表某类服务的调用方。</p> <p>Consumer 对象需要配置认证插件进行使用,以最简单的 <code>key-auth</code> 插件为例:</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: your-API-key' -X PUT -i -d ' { "username": "jack", "plugins": { "key-auth": { "key": "auth-jack" } }' </code></pre> <p>以上配置表示当请求中携带指定的 key(auth-jack)时,当前请求将会与 jack 这个消费者进行关联。可以看到,Consumer 上配置的认证插件实际上就是一个指定认证机制下的身份凭证,在 <code>key-auth</code> 插件中,key 即是标识某个消费者的凭证,类似的还有 <code>basic-auth</code> 插件的用户名与密码等等。</p> <h4>为 Route 配置认证插件</h4> <p>当我们通过 Consumer 将凭证信息与具体的消费者进行关联后,还需要在相应的路由上开启认证插件:</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9180/apisix/admin/routes/orders -H 'X-API-KEY: your-API-key' -X PUT -i -d ' { "uri": "/orders", "plugins": { "key-auth": { "header": "Authorization" } } }' </code></pre> <p>以上配置表示在 <code>/orders</code> 这个路由上开启 <code>key-auth</code> 插件,验证效果如下:</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/orders -H 'Authorization: auth-jack' -i HTTP/1.1 200 OK ... $ curl http://127.0.0.1:9080/orders -H 'Authorization: wrong-key' -i HTTP/1.1 401 Unauthorized ... {"message":"Invalid API key in request"} </code></pre> <p>当来自用户的请求命中这条路由时,APISIX 会尝试通过 <code>Authorization</code> 头部拿到用户提供的 Key,如果没有获取到,或者获取到的 Key 不是合法的,那么这个请求将会被网关直接拒绝,从而保护上游服务。</p> <p>可以看到以上两种认证方式中,认证插件都处于整个体系中的核心地位,它的丰富度直接影响着 API 网关用户对于认证方式的选择空间。下面将会介绍一些主流的认证方式,以及它们各自的优劣势,以供读者参考。</p> <h2>主流认证方式</h2> <p>认证鉴权作为计算机世界从第一天起就存在的基础机制,经过这么多年的迭代,已经发展成为一个非常多样化的领域。而 APISIX 的插件机制极大的降低了实现各种认证方式的开发成本,以下是部分 APISIX 已经支持的主流认证方式。</p> <h3>Key Auth</h3> <p>Key Auth 是所有认证插件中最简单的一款,但在实际环境中有着非常丰富的应用场景,例如:收费软件中的 license,开放 API 平台中的用于标识开发者的 token 等等,都可以非常轻松的使用 Key Auth 来实现。并且基于 APISIX 全动态的配置下发能力,Key 可以被迅速创建、吊销,而且实时生效。</p> <h3>Basic Auth</h3> <p>Basic Auth 是基于用户名、密码进行认证的方式,常用于网页登录场景,例如:网站的管理后台需要管理员登录后才可以使用,那么我们可以使用 Basic Auth 方式进行认证。</p> <h3>LDAP</h3> <p>LDAP(Lightweight Directory Access Protocol)是一种基于X.500 标准的轻量级文件访问协议,通过IP 协议提供访问控制和维护分布式信息的目录信息,借助于 LDAP ,运维人员可以细粒度地控制用户对资源的访问权限。通过 APISIX 的 <code>ldap-auth</code> 插件,可以轻松对接实现了 LDAP 协议的平台,例如微软的 Active Direcory,或者 Linux 平台的 OpenLDAP Server,从而能够精细化地控制 Consumer 对具体路由的访问权限。</p> <h3>OIDC</h3> <p>OpenID 是一个去中心化的网上身份认证系统。对于支持 OpenID 的网站,用户不需要记住像用户名和密码这样的传统验证标记。取而代之的是,他们只需要预先在一个作为 OpenID 身份提供者(identity provider, IdP)的网站上注册账号,而后就可以用这个账号登录所有对接了该提供者的应用,例如:可以通过知名的 Google 或者 Facebook 服务的账号来认证我们自身系统的用户。</p> <p>针对 OIDC,APISIX 提供了 <code>openid-connect</code> 插件,可以用于对接支持了 OIDC 协议的认证服务。</p> <h3>Forward Auth</h3> <p>当 APISIX 的标准认证插件无法满足你当前需求时,或者当前系统中已经部署了专门的并且是非标准协议的认证服务,此时你可以考虑使用 <code>forward-auth</code> 插件。使用该插件可以将用户的请求通过 HTTP 形式转发至认证服务中,并在认证服务响应非正常状态(错误码非 20x)时,返回自定义报错或者将用户重定向至认证页面。</p> <p>借助 <code>forward-auth</code> 插件的能力,可以非常巧妙地将认证与授权逻辑转移到专门的、非标准协议的外部服务中。</p> <h2>认证后与其他插件的联动</h2> <p>用户认证只是 APISIX 保障 API 安全的第一步,将认证能力与其他安全类型插件的有机结合将会进一步放大网关的安全能力。</p> <h3>ACL(consumer-restriction)</h3> <p>在一个复杂的后端系统中,可能会存在部分 API 的安全限制是高于其他 API 的,这种限制不仅需要拦截匿名用户,而且需要对认证用户进行限制,例如:只允许白名单用户访问用户管理 API。</p> <p><img src="https://static.apiseven.com/2022/11/17/637647759de62.jpg" alt="white list" referrerpolicy="no-referrer"></p> <p>此时我们可以使用 APISIX 提供的 <code>consumer-restriction</code> 插件去实现一个访问控制机制。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9180/apisix/admin/routes -H 'X-API-KEY: your-API-key' -X POST -i -d ' { "uri": "/api/v1/users/admin", "plugins": { "key-auth": {}, "consumer-restriction": { "whitelist": [ "Rose", "Peter ] } }, "upstream": { ... }, }' </code></pre> <p>上面的路由中通过 <code>key-auth</code> 和 <code>consumer-restriction</code> 两个插件,限制了 <code>/api/v1/users/admin</code> 路由需要通过 key auth 认证,并且仅 Rose 和 Peter 可以访问。</p> <h3>Rate Limiting (limit-count)</h3> <p>前面我们介绍了可以通过在 Consumer 中配置认证插件将用户凭证与消费者进行关联,事实 APISIX Consumer 对象不仅仅可以挂载认证类型的插件,而是可以像 Route 、Service 一样,挂载任意插件。</p> <p>以限流场景举例,在实际应用中,限流策略往往不是一成不变而是"千人千面",不同服务等级的调用方拥有不同的 API 限流策略是非常常见的需求,这样的需求是无法通过在路由上挂载限流插件进行解决的,我们需要在消费者上挂载限流插件,并且为每一个消费者指定不同的限流策略。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: your-API-key' -X PUT -i -d ' { "username": "jack", "plugins": { "key-auth": { "key": "jack" }, "limit-count": { "count": 200, "time_window": 60, "rejected_code": 503, "key": "$consumer_name", } }' $ curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: your-API-key' -X PUT -i -d ' { "username": "rose", "plugins": { "key-auth": { "key": "rose" }, "limit-count": { "count": 1000, "time_window": 60, "rejected_code": 503, "key": "$consumer_name", } }' </code></pre> <p>通过上面的配置,我们为 jack 和 rose 指定了不同的限流策略,rose 在 60 秒内拥有更多的请求次数配额 1000,而 jack 只有 200。</p> <h2>总结</h2> <p>认证鉴权作为 API 网关不可或缺的能力,它也是用户在选型 API 网关时考量的重要因素之一。</p> <p>本文中介绍的开源网关 Apache APISIX,覆盖了所有主流的认证方式,可以满足企业用户对于认证鉴权的需求。同时 APISIX 还拥有以下优势:</p> <ul> <li>丰富的、开箱即用的认证插件</li> <li>同时支持内置、外置认证方式,用户可以自由选择</li> <li>支持二次开发,方便对接自定义鉴权中心</li> </ul> <p>这些优势可以帮助企业更轻松的落地网关层面的认证鉴权,加强 API 安全。</p>

Gateway API 在 APISIX Ingress 的支持和使用

<h2>什么是 Kubernetes Gateway API</h2> <p>Gateway API 是除原生 Service 与 Ingress 之外,社区发起的另一个帮助用户将 Kubernetes 中的服务暴露到集群之外的规范,由 <a href="https://github.com/kubernetes/community/tree/master/sig-network">sig-network</a> 管理。</p> <p>Gateway API 包括了对许多常用网络协议(例如 HTTP、TCP、UDP)以及对 TLS 的支持。此外,Gateway API 包含的 Gateway 资源使得通过 Kubernetes API 来管理代理或网关的生命周期成为可能。</p> <h3>为什么需要 Gateway API</h3> <p>相比于 Ingress,Gateway API 代表了 Ingress 的功能父集。Gateway API 具有如下的改进:</p> <ul> <li>面向角色:Gateway 是由一组 API 资源组成的。不同的 API 资源代表了使用与配置 Kubernetes 网络资源的不同角色。</li> <li>表现力强:Gateway API 的核心功能就包含诸如基于头的匹配、流量加权以及其他在 Ingress 中只能通过各实现者自定义的非标准化 annotations 等方式实现的功能。</li> <li>可扩展:Gateway API 允许不同资源在不同层级一同使用。这使得能够对 API 结构进行更精细化的控制。</li> </ul> <p>此外,该标准还包含了可移植性、共享网关与跨命名空间引用等功能。</p> <p>如图所示,面向角色的设计使得不同团队之间可以可以共享集群内部的网络基础设施,并且共享集群管理员设置的策略与约束。这样,基础设施提供方、集群管理员、应用开发者等不同类型的角色可以只专注于自己的工作内容,而无需关注其他角色负责的部分。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/10/yRZrRX7E_gateway-1.png" alt="Gateway API model" referrerpolicy="no-referrer"></p> <p>不同的角色配置不同层级的 Gateway API 资源,不同层级的资源共同作用:</p> <p><img src="https://static.apiseven.com/uploads/2023/01/10/XREGZVIv_gateway-2.png" alt="Gateway API roles" referrerpolicy="no-referrer"></p> <h3>Gateway API 目前的状态</h3> <p>随着 Gateway API 被<a href="https://gateway-api.sigs.k8s.io/implementations/">广泛实现</a> 与应用,Gateway API 也发布了 v0.5.0 版本。在该版本中,一些核心的重要 API 首次进入 Beta 阶段,包括:GatewayClass、Gateway、HTTPRoute。</p> <h2>如何在 APISIX Ingress 中使用 Gateway API</h2> <p>APISIX Ingress Controller 对 Gateway API 的支持正在开发中,处于 Alpha 阶段,目前已支持 HTTPRoute、TCPRoute 等资源。</p> <h3>安装 Gateway API CRD</h3> <p>要使用 Gateway API,需要先安装 Gateway API 的 CRD,可通过 APISIX Ingress Controller 仓库下的副本或官方仓库 <a href="https://github.com/kubernetes-sigs/gateway-api/tree/main/config/crd/experimental">kubernetes-sigs/gateway-api</a>。此处使用 APISIX Ingress Controller 仓库的 CRD 副本为例。</p> <p>执行以下命令以安装 Gateway API 的 CRD:</p> <pre><code class="language-bash">git pull git@github.com:apache/apisix-ingress-controller.git cd apisix-ingress-controller kubectl apply -f ./samples/deploy/gateway-api/ </code></pre> <h3>安装 APISIX Ingress Controller</h3> <p>在 APISIX Ingress Controller 中,默认没有启用 Gateway API 的支持,可通过参数 <code>--enable-gateway-api=true</code> 启用。</p> <p>在使用 Helm 安装时,可通过配置 values 来启用。</p> <p>使用如下命令安装 APISIX 与 APISIX Ingress Controller:</p> <pre><code class="language-bash">helm repo add apisix https://charts.apiseven.com helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update kubectl create ns apisix-ingress helm install apisix apisix/apisix --namespace apisix-ingress \ --set gateway.type=NodePort \ --set ingress-controller.enabled=true \ --set ingress-controller.config.apisix.serviceNamespace=apisix-ingress \ --set ingress-controller.config.kubernetes.enableGatewayAPI=true </code></pre> <p>注意其中的参数 <code>--set ingress-controller.config.kubernetes.enableGatewayAPI=true</code> 即是用于开启 Gateway API 支持。</p> <p>这些命令将在 <code>apisix-ingress</code> 命名空间下创建完整的测试环境,包括 APISIX、etcd 与 APISIX Ingress Controller。</p> <h3>部署测试负载</h3> <p>使用 <code>kennethreitz/httpbin</code> 镜像作为测试负载。</p> <p>使用如下命中以在默认命名空间下部署这些负载:</p> <pre><code class="language-bash">kubectl run httpbin --image kennethreitz/httpbin --port 80 kubectl expose pod httpbin --port 80 </code></pre> <h3>配置 HTTPRoute</h3> <p>目前,APISIX Ingress Controller 支持 <code>v1alpha2</code> 版本的 Gateway API 资源。</p> <p>在测试时,使用如下 HTTPRoute 配置,将其保存到 <code>httproute.yaml</code> 文件中。</p> <pre><code class="language-yaml"># httproute.yaml apiVersion: gateway.networking.k8s.io/v1alpha2 kind: HTTPRoute metadata: name: basic-http-route spec: hostnames: - local.httpbin.org rules: - backendRefs: - name: httpbin port: 80 matches: - path: type: PathPrefix value: / </code></pre> <p>使用如下命令部署该 HTTPRoute 配置:</p> <pre><code class="language-bash">kubectl apply -f ./httproute.yaml </code></pre> <h3>验证</h3> <p>可以直接在 APISIX 的 Pod 中进行验证,执行以下命令:</p> <pre><code class="language-bash">kubectl -n apisix-ingress exec -it \ $(kubectl -n apisix-ingress get Pods -l "app.kubernetes.io/name=apisix" -o name) -c apisix -- \ curl -H "Host: local.httpbin.org" localhost:9080/ip </code></pre> <p>预期输出为:</p> <pre><code class="language-json">{ "origin": "127.0.0.1" } </code></pre> <p>这表明我们的配置成功生效。</p> <h3>APISIX Ingress 对 Gateway API 的支持状态</h3> <p>目前,APISIX Ingress Controller 正在对 Gateway API 添加支持,目前已经支持 HTTPRoute、TCPRoute 等资源。</p> <p>APISIX Ingress Controller 对于 Gateway 与 Gateway Class 的支持正在积极开发中,因此目前这些资源的配置暂时不会生效。</p> <h2>总结</h2> <p>在本文中,我们介绍了 Gateway API 这个社区中全新的将服务暴露到集群之外的规范,并且介绍了如何在 APISIX Ingress Controller 中使用它。</p> <p>APISIX Ingress Controller 对 Gateway API 的完整支持正在积极开发中。</p>

趣谈之什么是 API 货币化?

<h2>什么是 API 货币化</h2> <p>想象你开发并部署了一个服务,能够搜集你所在城市所有超市的打折和优惠信息,其他的开发者想要使用你的数据,就需要你提供相应的 API, 然后其他开发者通过支付费用获得你的授权,再使用你提供的 API 获取想要的数据,像这样通过 API 的方式将数据的使用转化为金钱就是 API 货币化,API 货币化是使你的服务盈利的一种理想方式。</p> <p><img src="https://static.apiseven.com/2022/10/28/635ba8294f703.png" alt="API货币化" referrerpolicy="no-referrer"></p> <p>在决定采用 API 货币化的商业模式后,接下来需要考虑的就是如何计费,可以选择按调用次数计费,或者直接订阅计费,但不管采用哪种计费方式,都需要统计不同用户的 API 调用数量,如果超出数量还需要进行限流限速操作,所以能否识别出用户的身份很关键。但仅仅识别出用户的个人身份还不够,因为往往购买服务的都是企业用户,企业员工在登录企业账号后,需要能够共享同一个计费账号,所以识别出用户所属组织也同样重要。</p> <h2>API 货币化的应用</h2> <p>现实生活中 API 货币化的应用无处不在,例如每个人都要接触的验证码功能,各个云厂商提供的消息队列,文字识别等服务,各个安全厂家提供的 WAF 和内容过滤等服务,这种模式是如此的成功,以至于我们迫切需要一个合适的技术栈来为 API 货币化打下坚实的基础,也就是说我们需要对 API 进行精细的管理。</p> <p>在管理 API 时,我们需要能够控制谁可以在哪里发布什么,并且要确保发布这些 API 符合组织标准,诸如 URL 模式、命名约定、访问控制规则。并且能让每个业务职能部门独立管理自己的 API,包括对已发布的 API 进行更新或设计改进,执行流量控制、速率限制和安全策略。也需要能够实时观测使用情况、性能及其他指标。</p> <p>要对 API 进行管理需要引入的工具就是 API 网关,API 网关可以帮助你解决管理 API 过程中遇到的各种问题。作为一个中央代理,API 网关将所有从客户端传入的请求路由到预定的目的地(后端服务),使你的 API 更安全和更容易管理,同时大部分 API 网关支持各种授权和认证协议,能够对 API 进行复杂的权限控制,还有速率限制等诸多功能。</p> <p>有许多流行的 API 网关开源项目,其中最为引人注目的就是 Apache APISIX 和其替代的企业 SaaS 解决方案 API7 Cloud。</p> <h2>APISIX 的 API 货币化实践</h2> <p>Apache APISIX 不仅支持上面提到的各种功能,还通过其丰富的插件,能够与 Prometheus、OpenTelemetry、Apache Skywalking 等多种可观察性平台进行集成,以进一步增强其分析 API 的能力并获得完整的可视性。同时 Apache APISIX 针对上文提到的识别用户身份,提出了 consumer 的概念。</p> <p><img src="https://static.apiseven.com/2022/10/28/635ba82754d89.png" alt="consumer" referrerpolicy="no-referrer"></p> <p>不同 consumer 对应不同的用户,通过在 consumer 上绑定对应的插件和上游,不同的 consumer 假如请求同一个 API,经用户认证体系识别后,网关服务根据当前请求用户信息,会对应不同的 Plugin 或 Upstream 配置,方便对不同的用户进行管理。</p> <p><img src="https://static.apiseven.com/2022/10/28/635ba829e693f.png" alt="consumer" referrerpolicy="no-referrer"></p> <p>但是仅仅支持 consumer 还不够,针对上文提到的企业用户,需要多个 consumer 共享同一个消费额度,并且如果只能分别管理每个 consumer 的配置,操作就太过繁琐了。因此 APISIX 提出了 consumer group 的概念,有了 consumer group,多个 consumer 就能共享同一套配置和同一个消费配额。</p> <p><img src="https://static.apiseven.com/2022/10/28/635ba8287f802.png" alt="consumer group" referrerpolicy="no-referrer"></p> <p>了解了 APISIX 在 API 货币化的实践,下面我们来看看具体的应用。</p> <ul> <li>给企业配置限流限速,企业的用户共享同一配置</li> </ul> <pre><code># create consumer group curl http://127.0.0.1:9180/apisix/admin/consumer_groups/company_a -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { "limit-count": { "count": 200, "time_window": 60, "rejected_code": 503, "group": "$consumer_group_id" } } }' # create consumer 1 curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "jack", "plugins": { "key-auth": { "key": "auth-one" } }, "group_id": "company_a" }' # create consumer 2 curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "johnson", "plugins": { "key-auth": { "key": "auth-two" } }, "group_id": "company_a" }' # create route curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "/get", "plugins": { "key-auth": {} }, "upstream": { "type": "roundrobin", "nodes": { "httpbin.org": 1 } } }' # hit the route curl -i http://127.0.0.1:9180/get -H 'apikey: auth-one' ... X-RateLimit-Limit: 200 X-RateLimit-Remaining: 199 ... curl -i http://127.0.0.1:9180/get -H 'apikey: auth-two' ... X-RateLimit-Limit: 200 X-RateLimit-Remaining: 198 ... # change count value to 2 requests per minute curl http://127.0.0.1:9180/apisix/admin/consumer_groups/company_a -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { "limit-count": { "count": 2, "time_window": 60, "rejected_code": 503, "group": "$consumer_group_id" } } }' # hit the route curl -i http://127.0.0.1:9180/get -H 'apikey: auth-two' ... X-RateLimit-Limit: 2 X-RateLimit-Remaining: 1 ... curl -i http://127.0.0.1:9180/get -H 'apikey: auth-one' ... X-RateLimit-Limit: 2 X-RateLimit-Remaining: 0 ... # no count, HTTP 503 curl -i http://127.0.0.1:9180/get -H 'apikey: auth-one' HTTP/1.1 503 Service Temporarily Unavailable # after a minute, count recover curl -i http://127.0.0.1:9180/get -H 'apikey: auth-one' ... X-RateLimit-Limit: 2 X-RateLimit-Remaining: 1 ... # create another route curl http://127.0.0.1:9180/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "/anything", "plugins": { "key-auth": {} }, "upstream": { "type": "roundrobin", "nodes": { "httpbin.org": 1 } } }' # you could see both routes share the same count curl -i http://127.0.0.1:9180/get -H 'apikey: auth-one' ... X-RateLimit-Limit: 2 X-RateLimit-Remaining: 1 ... curl -i http://127.0.0.1:9180/anything -H 'apikey: auth-one' HTTP/1.1 503 Service Temporarily Unavailable ... </code></pre> <h2>总结</h2> <p>企业通过 API 货币化将服务和数据转化为收入,要实现 API 货币化需要引入专业的 API 管理工具:API 网关,现在最热门的 API 网关是 APISIX,APISIX 在 API 货币化上有丰富的实践,包括 consumer, consumer group 等概念极大方便了用户对 API 的管理,赋能企业更顺畅的将 API 货币化落地。</p>

关于 OAuth 你又了解哪些?

<h2>OAuth 的背景</h2> <p>OAuth,O 是 Open,Auth 是授权,也就是开放授权的意思。OAuth 始于 2006 年,其设计初衷正是委托授权,就是让最终用户也就是资源拥有者,将他们在受保护资源服务器上的部分权限(例如查询当天订单)委托给第三方应用,使得第三方应用能够代表最终用户执行操作(查询当天订单)。</p> <p>OAuth 1.0 协议于 2010 年 4 月作为 RFC 5849 发布,这是一份信息性的评论请求。OAuth 2.0 框架的发布考虑了从更广泛的 IETF 社区收集的其他用例和可扩展性要求。尽管基于 OAuth 1.0 部署体验构建,OAuth 2.0 并不向后兼容 OAuth 1.0。OAuth 2.0 于 2012 年 10 月作为 RFC 6749 发布,承载令牌使用作为 RFC 6750 发布。</p> <p>在 OAuth 协议中,通过为每个第三方软件和每个用户的组合分别生成对受保护资源具有受限的访问权限的凭据,也就是访问令牌,来代替之前的用户名和密码。而生成访问令牌之前的登录操作,又是在用户跟平台之间进行的,第三方软件根本无从得知用户的任何信息。</p> <p>这样第三方软件的逻辑处理就大大简化了,它今后的动作就变成了请求访问令牌、使用访问令牌、访问受保护资源,同时在第三方软件调用大量 API 的时候,不再传输用户名和密码,从而减少了网络安全的攻击面。</p> <p>说白了就是集中授权。</p> <p>值得注意的是,OAuth 并非身份验证,这里的 Auth 是 Authorization,OAuth 是发生在用户做了身份验证后的事情,系统授权用户能做什么操作。互联网中所有的受保护资源,几乎都是以 Web API 的形式来提供访问的。不同的用户能做的事情不同,例如一个 GitHub 项目,有些用户只有读取和提交 PR(pull request)的权限,而管理员用户则能合并 PR。将用户权限在 API 层面细分,是 OAuth 要做的事情。</p> <h2>OAuth的授权流程</h2> <h3>角色</h3> <p>在 OAuth 2.0 的体系里面有四种角色:</p> <ul> <li> <p>第三方应用:一般分为前端浏览器、APP 和后端应用服务器。</p> </li> <li> <p>资源拥有者:使用第三方应用的用户,并在授权服务器上有账号。</p> </li> <li> <p>授权服务:提供授权的开发平台,例如微博、GitHub、微信。</p> </li> <li> <p>受保护资源:用户的各类信息,例如用户名、头像、昵称、邮箱等信息。</p> </li> </ul> <h3>流程</h3> <p><img src="https://static.apiseven.com/2022/11/01/6360ee518d404.png" alt="oauth flow1" referrerpolicy="no-referrer"></p> <p>步骤A:第三方应用向用户(其实是通过授权服务器)申请授权码</p> <p>步骤B:授权服务器返回授权码给第三方应用</p> <p>步骤C:第三方应用将授权码发给资源服务器,申请访问口令</p> <p>步骤D:授权服务器返回访问口令给第三方应用</p> <p>步骤E:第三方应用使用访问口令向资源服务器请求用户信息</p> <p>步骤F:资源服务器返回用户信息,第三方应用提供业务逻辑给用户</p> <h4>授权码和访问口令</h4> <p>获取访问口令的方式在标准里有四种,这里只谈论授权码方式,这也是最常见最安全的方式:</p> <p><img src="https://static.apiseven.com/2022/11/01/6360ee4fbbc3a.png" alt="oauth flow2" referrerpolicy="no-referrer"></p> <p>步骤A:第三方应用让用户选择授权方式,例如 GitHub,然后携带<code>client_id</code>和<code>redirect_uri</code>等参数将用户重定向到授权服务器</p> <p>请求示例:</p> <pre><code> GET /authorize?response_type=code&amp;client_id=s6BhdRkqt3&amp;state=xyz &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com </code></pre> <p>步骤B:用户登录和授权</p> <p>步骤C:授权服务器根据<code>redirect_uri</code>将用户重定向回到第三方应用的后端,提供授权码</p> <p>响应示例:</p> <pre><code> HTTP/1.1 302 Found Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA &amp;state=xyz </code></pre> <p>步骤D:第三方应用的后端访问授权服务器,用授权码去换访问口令</p> <p>请求示例:</p> <pre><code> POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&amp;code=SplxlOBeZQQYbYS6WxSbIA &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb </code></pre> <p>步骤E:授权服务器返回访问口令,第三方应用的后端渲染功能页面(对应步骤C)给浏览器,为用户提供功能</p> <p>授权服务器的响应示例:</p> <pre><code> HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" } </code></pre> <h4>实际场景示例</h4> <p>小明想通过小兔软件打印他在京东上的订单。</p> <p>资源拥有者 -&gt; 小明</p> <p>第三方软件 -&gt; 小兔软件</p> <p>授权服务 -&gt; 京东商家开放平台的授权服务</p> <p>受保护资源 -&gt; 小明店铺在京东上面的订单</p> <p><img src="https://static.apiseven.com/2022/11/01/6360ee51138d8.png" alt="oauth example" referrerpolicy="no-referrer"></p> <h4>为什么授权码和访问口令要分开获取呢?</h4> <p>OAuth2 协议中,用户登录成功后,OAuth2 认证服务器会将用户的浏览器回调到一个回调地址,并携带一个授权码 <code>code</code>。这个授权码 <code>code</code> 一般有效期十分钟且一次有效,用后作废。这避免了在前端暴露 <code>access_token</code> 或者用户信息的风险,<code>access_token</code> 的有效期都比较长,一般为 1~2 个小时。如果泄露会对用户造成一定影响。后端收到这个 <code>code</code> 之后,需要使用 <code>Client Id + Client Secret + Code</code> 去授权服务器换取用户的 <code>access_token</code>。</p> <p>在这一步,实际上授权服务器对第三方应用进行了认证,能够确保来授权服务器获取 <code>access_token</code> 的机器是可信任的,而不是任何一个人拿到 <code>code</code> 之后都能来授权服务器进行 <code>code</code> 换 <code>token</code>。如果 <code>code</code> 被黑客获取到,如果他没有 <code>Client Id + Client Secret</code> 也无法使用,就算有,也要和真正的应用服务器竞争,因为 <code>code</code> 一次有效,用后作废,加大了攻击难度。相反,如果不经过 <code>code</code> 直接返回 <code>access_token</code> 或用户信息,那么一旦泄露就会对用户造成影响。</p> <p>简单说就是,client secret 不能暴露给前端(验证 client),用户授权(获取 code)又只能前端做,因此需要分两步。</p> <h2>OIDC(OpenID Connect)</h2> <p>既然 OAuth 本身就隐含了身份验证,那么为什么不以标准化的形式将身份验证的结果导出,使得第三方应用可以使用呢?这就是 OIDC 要做的事情了。那身份验证的结果是什么?很简单,它就是用户的各种信息。</p> <p>OIDC 怎么做?简单来说,就是在 OAuth 返回 <code>access token</code> 的时候顺带返回 <code>id token</code>,<code>id token</code> 的格式是 <code>JWT</code>,第三方应用可使用非对称公钥或者对称密码验证 <code>id token</code> 的合法性和有效性,而 <code>id token</code> 本身也包含了基本用户信息。另外,OIDC 提供了 UserInfo endpoint,第三方应用可携带 access token 访问该 endpoint 以获取额外的用户信息。</p> <p>OIDC 还有一个好处,就是单点登录(SSO,Single Sign On)和单点注销(SLO,Single LogOut)。跟 OAuth 类似,OIDC 提供的集中化身份验证,它可以对应多个应用。只要用户成功登录了一个应用,那么当他登录其他应用的时候,就无需再进行一次身份验证了(例如输入用户名密码),那是因为授权服务器在用户的浏览器里面存下了 cookie。而单点注销则是用户注销了一个应用,其他应用也顺便注销了,注销既可以借由浏览器来做,也可以由第三方应用的后端与授权服务器之间来做。注销的时候指定的参数就是 <code>id token</code> 里面的 session 字段。</p> <p>注意 OIDC 并没有指定身份验证的具体方式,例如传统的密码或者刷脸,而是指定了如何将身份验证委托给一个集中化的身份验证提供者,在身份验证通过后得到什么凭证(<code>id token</code>),这个凭证如何被校验(JWT 格式),这个凭证包含了哪些用户信息。这样第三方应用就无需重造轮子了。OAuth 提供了集中化的授权,而 OIDC 则是在此基础上进一步提供了集中化的身份验证。</p> <h2>APISIX 对 OAuth/OIDC 的支持</h2> <p>Apache APISIX 是一个开源的云原生 API 网关,作为 API 网关,它兼具动态、实时、高性能等特点,提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。你可以使用 APISIX 来处理传统的南北向流量,以及服务间的东西向流量,也可以当做 K8s Ingress controller 来使用。</p> <p>APISIX 既然是 API 网关,为多个上游应用服务器做代理,那么集中授权、集中身份认证,放在 API 网关是最自然不过的事情了。</p> <p>APISIX 的 <code>openid-connect</code> 插件支持 OpenID Connect 协议,用户可以使用该插件让 APISIX 对接众多认证鉴权软件,如 Okta、Keycloak、Ory Hydra、Authing 等,作为集中式认证网关部署于企业中。OIDC是OAuth的超集,所以这个插件也隐含了对OAuth的支持。</p> <p>部署图如下所示:</p> <p><img src="https://static.apiseven.com/2022/11/01/6360ee5f5d57a.png" alt="apisix oauth" referrerpolicy="no-referrer"></p> <h3>配置实例:使用 Keycloak 与 API 网关保护你的 API</h3> <h4>配置 Keycloak</h4> <table> <thead> <tr> <th>信息</th> <th>取值</th> </tr> </thead> <tbody> <tr> <td>keycloak地址</td> <td>http://127.0.0.1:8080/</td> </tr> <tr> <td>Realm</td> <td>myrealm</td> </tr> <tr> <td>Client Type</td> <td>OpenID Connect</td> </tr> <tr> <td>Client ID</td> <td>myclient</td> </tr> <tr> <td>Client Secret</td> <td>e91CKZQwhxyDqpkP0YFUJBxiXJ0ikJhq</td> </tr> <tr> <td>Redirect URI</td> <td>http://127.0.0.1:9080/anything/callback</td> </tr> <tr> <td>Discovery</td> <td>http://127.0.0.1:8080/realms/myrealm/.well-known/openid-configuration</td> </tr> <tr> <td>Logout URI</td> <td>/anything/logout</td> </tr> <tr> <td>Username</td> <td>myuser</td> </tr> <tr> <td>Password</td> <td>myrealm</td> </tr> <tr> <td>Realm</td> <td>mypassword</td> </tr> </tbody> </table> <h4>场景示例</h4> <pre><code class="language-bash">curl -XPUT 127.0.0.1:9080/apisix/admin/routes/1 -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -d '{ "uri":"/anything/*", "plugins": { "openid-connect": { "client_id": "myclient", "client_secret": "e91CKZQwhxyDqpkP0YFUJBxiXJ0ikJhq", "discovery": "http://127.0.0.1:8080/realms/myrealm/.well-known/openid-configuration", "scope": "openid profile", "bearer_only": false, "realm": "myrealm", "redirect_uri": "http://127.0.0.1:9080/anything/callback", "logout_path": "/anything/logout" } }, "upstream":{ "type":"roundrobin", "nodes":{ "httpbin.org:80":1 } } }' </code></pre> <p>创建 API 成功后访问 http://127.0.0.1:9080/anything/test 时,由于未进行登录,因此将被引导到 Keycloak 的登录页面:</p> <p><img src="https://static.apiseven.com/2022/11/01/6360ee5e3f870.png" alt="apisix keycloak login" referrerpolicy="no-referrer"></p> <p>输入账号(myuser)、密码(mypassword)完成登录后,成功跳转到 http://127.0.0.1:9080/anything/test 页面:</p> <p><img src="https://static.apiseven.com/2022/11/01/6360ee640f76d.png" alt="apisix keycloak authorized" referrerpolicy="no-referrer"></p> <p>访问 http://127.0.0.1:9080/anything/logout 退出登录:</p> <p><img src="https://static.apiseven.com/2022/11/01/6360ee619c380.png" alt="apisix keycloak logout" referrerpolicy="no-referrer"></p> <h2>深入阅读</h2> <ul> <li> <p><a href="https://apisix.apache.org/zh/blog/2022/07/06/use-keycloak-with-api-gateway-to-secure-apis/">使用 Keycloak 与 API 网关保护你的 API</a></p> </li> <li> <p><a href="https://apisix.apache.org/zh/blog/2022/07/04/apisix-integrates-with-hydra/">APISIX 与 Ory Hydra 集成丰富身份认证方式</a></p> </li> <li> <p><a href="https://apisix.apache.org/zh/docs/apisix/plugins/openid-connect/">APISIX openid-connect插件</a></p> </li> </ul>

为什么 APISIX Ingress 是比 Traefik 更好的选择?

<h2>Apache APISIX Ingress 是什么</h2> <p><a href="https://github.com/apache/apisix-ingress-controller/">Apache APISIX Ingress</a> 是一个使用 Apache APISIX 作为数据面的 Kubernetes Ingress controller 实现。</p> <p>支持多种规则的配置方式:</p> <ul> <li>Ingress</li> <li>APISIX Ingress CRD (自定义资源)</li> <li>Gateway API</li> </ul> <p>其整体采用数据面和控制面分离的架构,由 Apache APISIX 承载实际的业务流量,所以这可以大大提升整体的安全性,极大的避免了由于数据面被攻击而导致 Kubernetes 集群被攻击的可能。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/10/5o2kkCj9_module-0.png" alt="APISIX Ingress architecture" referrerpolicy="no-referrer"></p> <h2>Traefik 是什么</h2> <p>Traefik 是由 Traefik Labs 开源的一款反向代理和负载均衡器。</p> <p>在 Kubernetes 中支持多种规则的配置方式:</p> <ul> <li>Ingress</li> <li>Traefik IngressRoute (自定义资源)</li> <li>Gateway API</li> </ul> <p>Traefik 是一个统一的二进制文件,控制面和数据面的代理逻辑都在一起。如果受到攻击或者有远程执行的安全漏洞被利用,可能存在 Kubernetes 集群被攻击的情况。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/10/r7lpnTtO_traefik.png" alt="Traefik architecture" referrerpolicy="no-referrer"></p> <h2>对比</h2> <p>接下来笔者将从以下几个维度对 Apache APISIX Ingress 和 Traefik 做一些对比。</p> <h3>协议支持</h3> <p>作为网关,最为核心的能力便是要能够正确的代理流量。作为 Kubernetes 集群的入口网关,主要处理如下两部分的流量:</p> <ul> <li>Client 到网关的流量;</li> <li>网关与 Upstream 的流量;</li> </ul> <p>如下所示:</p> <pre><code>Client &lt;----&gt; Ingress &lt;----&gt; Upstream Service </code></pre> <p>当前的协议多种多样,以下是个简单的列表供读者进行对比。</p> <table> <thead> <tr> <th style="text-align:center">协议</th> <th style="text-align:center">APISIX Ingress</th> <th style="text-align:center">Traefik</th> </tr> </thead> <tbody> <tr> <td style="text-align:center">HTTP/HTTPS</td> <td style="text-align:center">支持</td> <td style="text-align:center">支持</td> </tr> <tr> <td style="text-align:center">HTTP/2</td> <td style="text-align:center">支持</td> <td style="text-align:center">支持</td> </tr> <tr> <td style="text-align:center">HTTP/3</td> <td style="text-align:center">不支持</td> <td style="text-align:center">支持</td> </tr> <tr> <td style="text-align:center">TCP</td> <td style="text-align:center">支持</td> <td style="text-align:center">支持</td> </tr> <tr> <td style="text-align:center">UDP</td> <td style="text-align:center">支持</td> <td style="text-align:center">支持</td> </tr> <tr> <td style="text-align:center">WebSocket</td> <td style="text-align:center">支持</td> <td style="text-align:center">支持</td> </tr> <tr> <td style="text-align:center">Dubbo</td> <td style="text-align:center">支持</td> <td style="text-align:center">不支持</td> </tr> </tbody> </table> <p>此外,无论是 APISIX Ingress 还是 Traefik 均可通过 HTTP/2 或者 TCP 代理等方式支持 gRPC、MQTT 等协议,故而未在上述表格中列出。</p> <p>从协议支持的角度来看,APISIX Ingress 和 Traefik 各有优势。APISIX 对于 HTTP/3 的支持正在规划中。</p> <h3>可扩展性</h3> <p>由于业务需求多种多样,所以可扩展性也是进行技术选型的一项主要指标。Apache APISIX Ingress 和 Traefik 均提供了一些扩展方式, 以下将分别进行介绍。</p> <h4>Apache APISIX Ingress</h4> <p>在 APISIX Ingress 中进行功能扩展,主要是通过开发自定义 Plugin 完成的。当前主要支持如下几种 Plugin 的开发方式:</p> <ul> <li>通过 Lua 进行插件的开发:这种方式相对简单,并且几乎没有性能损耗;</li> <li>通过 *-plugin-runner 开发:这种模式下支持 JAVA/Python/Go 等语言进行开发,这可以方便用户利用一些现有的业务逻辑,并且无需学习新语言;</li> <li>通过 WASM 进行插件插件:这种模式下,可以使用任何支持构建出 WASM 的语言进行插件开发;</li> </ul> <p>此外还可以通过 serverless plugin 来直接书写 Lua 代码,快速的满足业务需求。</p> <p>当然,如果你有 Lua 模块的开发经验,也可以直接写 Lua 模块,然后进行加载即可,增加如下配置即可:</p> <pre><code class="language-yaml">apisix: ... extra_lua_path: "/path/to/example/?.lua" </code></pre> <p>具体的 Plugin 开发步骤和使用,请参考 <a href="https://apisix.apache.org/docs/apisix/plugin-develop/">Apache APISIX 的插件开发文档</a> 和 <a href="https://api7.ai/blog/how-does-apisix-ingress-support-custom-plugins">APISIX Ingress 如何支持自定义插件</a>。</p> <h4>Traefik</h4> <p>Traefik 也提供了 Plugin 机制用于对功能进行一些扩展。但是 Traefik 是由 Go 进行开发的,它的插件同样也是用 Go 进行开发的。</p> <p>在开发完就可以在 Traefik 的配置中添加如下内容进行引用了。如下:</p> <pre><code class="language-yaml">experimental: localPlugins: example: moduleName: github.com/traefik/pluginproviderdemo </code></pre> <p>这里需要注意,Plugin 的名字,需要与包名保持一致。</p> <p>总体来说,APISIX Ingress 提供了更多种的扩展方式,可以根据实际情况进行灵活选择,选择自己最喜欢/擅长的工具即可,而且也更容易与自己的现有业务进行集成。 而 Traefik 仅支持通过 Go 语言进行开发。</p> <h3>生态</h3> <p>在进行技术选型的时候,主要会考虑项目所使用的协议,项目的归属等方面,以及与现有基础设施是否可以整合。</p> <table> <thead> <tr> <th style="text-align:center">对比维度</th> <th style="text-align:center">APISIX Ingress</th> <th style="text-align:center">Traefik</th> </tr> </thead> <tbody> <tr> <td style="text-align:center">归属</td> <td style="text-align:center">Apache 软件基金会(ASF)</td> <td style="text-align:center">Traefik Labs</td> </tr> <tr> <td style="text-align:center">协议</td> <td style="text-align:center">Apache 2.0</td> <td style="text-align:center">MIT</td> </tr> <tr> <td style="text-align:center">诞生时间</td> <td style="text-align:center">2019 年 6 月</td> <td style="text-align:center">2015 年 8 月</td> </tr> <tr> <td style="text-align:center">consul</td> <td style="text-align:center">支持</td> <td style="text-align:center">支持</td> </tr> <tr> <td style="text-align:center">nacos</td> <td style="text-align:center">支持</td> <td style="text-align:center">不支持</td> </tr> <tr> <td style="text-align:center">Eureka</td> <td style="text-align:center">支持</td> <td style="text-align:center">不支持</td> </tr> <tr> <td style="text-align:center">etcd</td> <td style="text-align:center">支持</td> <td style="text-align:center">支持</td> </tr> <tr> <td style="text-align:center">zookeeper</td> <td style="text-align:center">支持</td> <td style="text-align:center">支持</td> </tr> <tr> <td style="text-align:center">DNS</td> <td style="text-align:center">支持</td> <td style="text-align:center">不支持</td> </tr> </tbody> </table> <p>上述对比同时包含了控制面和数据面方面的内容。此外,这两个项目都非常积极与一些周边项目进行了集成和合作,比如 Rancher、KubeSphere 等。</p> <p>从这个角度来看,APISIX Ingress 相比于 Traefik 提供了更为广泛的与基础组件的集成能力。在进行技术选型时,可以结合当前自己所用的基础组件的情况进行权衡。</p> <h2>来自用户的声音(用户案例)</h2> <p><a href="https://api7.ai/blog/why-horizon-robotics-migrated-from-traefik-to-apche-apisix">地平线使用 APISIX Ingress 替换了 Traefik</a>,主要是考虑如下方面:</p> <ul> <li>通过 Annotation 增加的配置不易重用;</li> <li>Traefik 中默认的行为与 NGINX 中不同,用户在使用时候会产生困惑;</li> </ul> <p>在切换为 Apache APISIX Ingress 后,得益于 APISIX Ingress 丰富的插件生态,绝大多数需求均可通过内置插件满足。并且插件的配置可直接通过 APISIX Ingress 的 ApisixRoute 资源进行定义,比较直观。也可以通过 ApisixPluginConfig 进行插件模板的配置,在其他的 ApisixRoute 资源中进行引用。</p> <p><img src="https://static.apiseven.com/uploads/2023/01/10/ymUiJzTY_ingress-usecase.webp" alt="地平线架构图" referrerpolicy="no-referrer"></p> <p>APISIX Ingress 的数据面性能更佳,能高效的应对日益增长的业务流量,而不会陷入性能瓶颈。</p> <p>除去地平线以外,包括<a href="http://www.igetcool.com/">少年得到</a>,<a href="http://www.gwwisdom.com/">观为智慧</a>等公司也都使用 APISIX Ingress 替换了 Traefik,更多用户案例请参考<a href="https://api7.ai/category/usercase">用户案例</a>。</p> <p>此外,Apache APISIX 社区非常活跃,在 GitHub、Slack 等频道上,都可以很快的得到响应,或找到解决办法。</p> <h2>总结</h2> <p>本篇笔者从协议支持,可扩展性和生态等方面对比了 Apache APISIX Ingress 和 Traefik,从中可以看到 APISIX Ingress 在可扩展性和生态集成方面有一定的优势,用户可以更容易的对 APISIX Ingress 进行扩展,以及和一些基础组件进行集成。希望能为读者在进行 Kubernetes Ingress controller 选型时提供一些帮助。</p>

马斯克都不懂的 GraphQL,API 网关又对其如何理解?

<h2>什么是 GraphQL?它有多流行?</h2> <p>GraphQL 是一套由 Facebook 在 2015 年发布的一套面向 API 的查询操作语言。相比于其他的 API 设计方式,GraphQL 允许客户端根据事先约定的数据结构组建查询语句,由服务端解析这一语句并只返回所需的内容。这么一来,GraphQL 在提供丰富性和灵活性的同时,避免了冗余数据带来的性能损耗。GraphQL 的这一特性,让它在需要跟许多复杂数据对象打交道的应用场景里大行其道,成为该环境下的不二之选。</p> <p>在 2018 年,GraphQL 完成了规范的制定工作,并推出了稳定版本。同年,Facebook 将 GraphQL 项目捐献给了 Linux 基金会下属的 GraphQL 基金会。自那以后,GraphQL 已经在许许多多的开源项目和商业机构中落地。到目前为止,市面上已经有了多个 GraphQL 的主流客户端实现。而服务端的实现遍布各大服务端编程语言,甚至连一些小众编程语言如 D 和 R 都有对应的实现。</p> <h2>GraphQL 的一些真实场景,和(同样真实的)挑战</h2> <p>最为知名的采用 GraphQL 的例子,莫过于 GitHub 的 GraphQL API 了。</p> <p>在拥抱 GraphQL 之前,GitHub 提供了 REST API 来暴露千千万万托管项目所产生的丰富数据。GitHub 的 REST API 是如此的成功,以致于它成为了人们设计 REST API 时竞相模仿的典范。然而随着数据对象的变多和对象内字段的变大,REST API 开始暴露出越来越多的弊端。在服务端,由于每次调用都会产生大量的数据,GitHub 为了降低成本不得不对调用频率设置严格的限制。而在开发者这一边,他们则不得不与这一限制做斗争。因为虽然单次调用会返回繁多的数据,但是绝大部分都是无用的。开发者要想获取某一特定的信息,往往需要发起多个查询,然后编写许多胶水代码把查询结果中有意义的数据拼接成所需的内容。在这一过程中,他们还不得不带上“调用次数”的镣铐。</p> <p>所以 GraphQL 的出现,立刻就让 GitHub <a href="https://docs.github.com/en/graphql/overview/about-the-graphql-api#why-github-is-using-graphql">皈依</a>了。GitHub 成为了 GraphQL 的使者保罗,为万千开发者传递福音。目前 GraphQL API 已经是 GitHub API 的首选。从<a href="https://github.blog/2016-09-14-the-github-graphql-api/">第一次宣布对 GraphQL 的支持</a>之后,GitHub 每一年都会<a href="https://github.blog/?s=graphQL">发几篇关于 GraphQL 的文章</a>。为了让开发者能够迁移到 GraphQL 上来,GitHub 专门写了个交互式查询应用:https://docs.github.com/en/graphql/overview/explorer。开发者可以通过这个应用学习怎么编写 GraphQL。</p> <p>然而 GraphQL 并非灵丹妙药。就在最近,<a href="https://github.blog/changelog/2022-08-18-deprecation-notice-graphql-for-packages/">GitHub 废弃自己的 package API 的 GraphQL 实现</a>。许多人也开始<a href="https://honest.engineering/posts/why-use-graphql-good-and-bad-reasons">热议 GraphQL 的一些缺点</a>。GraphQL 的许多问题源自于它跟 HTTP 标准的结构差别较大,没办法简单地将 GraphQL 的一些概念映射到诸如 HTTP path/header 这样的结构中。把 GraphQL 当作普通的 HTTP API 来处理,需要额外的开发工作。如此一来,开发者如果要管理自己的 GraphQL API,就必须采用支持 GraphQL 的 API 网关才行。</p> <h2>APISIX 现在对 GraphQL 的支持</h2> <p>APISIX 目前支持通过 GraphQL 的一些属性进行动态路由。通过该能力,我们可以只接受特定的 GraphQL 请求,或者让不同的 GraphQL 转发到不同的上游。</p> <p>以下面的 GraphQL 语句为例:</p> <pre><code> query getRepo { owner { name } repo { created } } </code></pre> <p>APISIX 会提取 GraphQL 以下三个属性,用在路由当中:</p> <ul> <li>graphql_operation</li> <li>graphql_name</li> <li>graphql_root_fields</li> </ul> <p>在上面的 GraphQL 语句中:</p> <ul> <li><code>graphql_operation</code> 对应 <code>query</code></li> <li><code>graphql_name</code> 对应 <code>getRepo</code></li> <li><code>graphql_root_fields</code> 对应 <code>["owner", "repo"]</code></li> </ul> <p>让我们来创建一个路由,展示下 APISIX 对 GraphQL 的精细化路由能力。</p> <pre><code>curl http://127.0.0.1:9180/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "methods": ["POST"], "uri": "/graphql", "vars": [ ["graphql_operation", "==", "query"], ["graphql_name", "==", "getRepo"], ["graphql_root_fields", "has", "owner"] ], "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:2022": 1 } } }' </code></pre> <p>接下来使用带有 GraphQL 语句的请求去访问:</p> <pre><code>curl -i -H 'content-type: application/graphql' \ -X POST http://127.0.0.1:9080/graphql -d ' query getRepo { owner { name } repo { created } }' HTTP/1.1 200 OK ... </code></pre> <p>我们可以看到请求到达了上游。这是因为查询语句匹配了全部三个条件。 反之,如果我们使用不匹配的语句来访问,比如不包含 owner 字段:</p> <pre><code>curl -i -H 'content-type: application/graphql' \ -X POST http://127.0.0.1:9080/graphql -d ' query getRepo { repo { created } }' HTTP/1.1 404 Not Found ... </code></pre> <p>则不会匹配对应的路由规则。</p> <p>我们可以另外创建一个路由,让不包含 owner 字段的语句路由到别的上游:</p> <pre><code>curl http://127.0.0.1:9180/apisix/admin/routes/2 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "methods": ["POST"], "uri": "/graphql", "vars": [ ["graphql_operation", "==", "query"], ["graphql_name", "==", "getRepo"], ["graphql_root_fields", "!", "has", "owner"] ], "upstream": { "type": "roundrobin", "nodes": { "192.168.0.1:2022": 1 } } }' </code></pre> <pre><code>curl -i -H 'content-type: application/graphql' \ -X POST http://127.0.0.1:9080/graphql -d ' query getRepo { repo { created } }' HTTP/1.1 200 OK ... </code></pre> <h2>展望 APISIX 未来对 GraphQL 的支持</h2> <p>除了动态路由之外,APISIX 在未来也可能会根据 GraphQL 的具体字段推出更多操作。比如说,GitHub 的 GraphQL API 有<a href="https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit">专门一套针对限流的计算公式</a>,我们也可以应用类似的规则来把单个 GraphQL 请求转成相应的“虚拟调用”次数,来完成 GraphQL 专属的限流工作。</p> <p>我们也可以换个思路解决问题,应用自身还是提供 REST API,由网关在最外层把 GraphQL 请求转成 REST 请求,把 REST 响应转成 GraphQL 响应。这种方式提供的 GraphQL API 无需开发专门的插件,就可以完成诸如 RBAC、限流、缓存等功能。因为在后续的插件看来,它就是个平平无奇的 REST API。从技术角度上讲,这个思路并不难实现。毕竟在 2022 年的现在,即使 REST API 往往也会提供 OpenAPI spec 来作为 schema,无非是 GraphQL schema 和 OpenAPI schema 之间的互转,外加 GraphQL 特有的字段筛选罢了。(当然,我必须承认,我并没有亲自实践过,或者在一些细节上存在尚待克服的挑战)</p> <p>细心的读者会发现,这种方式转换得来的 GraphQL API,每次只能操作一个模型,显然无法满足 GraphQL 灵活性的要求,无非是披着 GraphQL 外衣的 REST API。且慢,我还没有把话说完呢!GraphQL 有一个叫 <a href="https://www.apollographql.com/blog/backend/graphql-schema-stitching/">schema stitch</a> 的概念,允许实现者把多个 schema 组合在一起。</p> <p>举个例子,我们有两个 API,一个叫 GetEvent,另一个叫 GetLocation。他们返回的类型分别是 Event 和 Location。</p> <pre><code>type Event { id: string location_id: string } type Location { id: string city: string } type Query { GetEvent(id: string): Event GetLocation(id: string): Location } </code></pre> <p>我们可以加一个配置,由这两个 API 组合成新的 API 叫 GetEventWithLocation。新的 API 是这样的:</p> <pre><code>type EventWithLocation { id: string location: Location } type Query { GetEventWithLocation(id: string): EventWithLocation } </code></pre> <p>stitch 的具体实现由网关来完成。在上面的例子中,网关会把 API 拆成两个,先调用 GetEvent 得到 location_id,再调用 GetLocation 得到组合后的数据。</p> <p>总而言之,通过 REST 转 GraphQL,每个 REST API 可以变成对应的 GraphQL 模型;再借助 schema stitch,可以把多个模型组合成一个 GraphQL API。这么一来,我们就能在现有的 REST API 上构建起丰富灵活的 GraphQL API,且在 REST API 的粒度上完成具体的插件管理。这一设计顺带解决了部分 API 编排的问题。就像上面的例子中,我们把一个 API 的输出(Event.location_id)作为另一个 API 的输入(Location.id)。</p>

APISIX Ingress 如何使用 cert-Manager 管理证书

<h2>cert-manager 解决了什么问题</h2> <p><a href="https://cert-manager.io/">Cert-manager</a> 是由 <a href="https://www.jetstack.io/">JETSTACK</a> 在 2017 年开源,并在 2020 年 11 月正式捐赠给了 CNCF 成为 sandbox 级别的项目。在 2022 年 10 月正式成为 CNCF incubating 级别的项目。</p> <p>Cert-manager 主要用来自动化的管理 Kubernetes 和 OpenShift 中的 x509 证书身份,它使证书和证书签发结构成为了 Kubernetes 上第一类支持的资源(通过 CRD 实现),并且允许开发者可以很简单的为应用程序申请证书,以便提升应用访问的安全性。</p> <p>那么我们来看看在 cert-manager 出现之前,在 Kubernetes 中如何管理证书呢?</p> <h3>Kubernetes 中的证书如何管理</h3> <p>在 Kubernetes 中存储数据主要有以下两种原生的方式:</p> <ul> <li> <p>ConfigMap</p> </li> <li> <p>Secret</p> </li> </ul> <p>不过 ConfigMap 中所有的信息都是明文的,存储一些相对普通的配置信息还可以,但对于证书这类比较私密的信息,就不那么适用了。</p> <p>Kubernetes 在设计的时候就推荐使用 Secret 来存储证书等相关信息,并且还为此提供了默认支持。我们可以很简单的通过 <code>kubectl create secret tls</code> 来存储证书的信息。比如:</p> <pre><code class="language-Bash">➜ ~ kubectl create secret tls moelove-tls --cert=./cert.pem --key=./cert-key.pem secret/moelove-tls created ➜ ~ kubectl get secret moelove-tls -oyaml apiVersion: v1 data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNFekNDQWJtZ0F3SUJBZ0lVVHhCTC9aQkdpOEJCOUFVN2JRWi9jK3c2L1Rzd0NnWUlLb1pJemowRUF3SXcKVFRFTE1Ba0dBMVVFQmhNQ1EwNHhFREFPQmdOVkJBY1RCMEpsYVdwcGJtY3hGVEFUQmdOVkJBb1RERTF2WlV4dgpkbVVnU1U1R1R6RVZNQk1HQTFVRUF4TU1iVzlsYkc5MlpTNXBibVp2TUI0WERUSXlNVEF4T1RBM01UY3dNRm9YCkRUSXpNVEF4T1RBM01UY3dNRm93VFRFTE1Ba0dBMVVFQmhNQ1EwNHhFREFPQmdOVkJBY1RCMEpsYVdwcGJtY3gKRlRBVEJnTlZCQW9UREUxdlpVeHZkbVVnU1U1R1R6RVZNQk1HQTFVRUF4TU1iVzlsYkc5MlpTNXBibVp2TUZrdwpFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVVTcEFjNGE1UXQwQ0NVa2hGSGY3WnZvR1FReVVPUUxSClJhZG0rSUUrV1ZkOThyWkc5NFpob08ybDZSWkY2MnVPN3FpZ2VsaUJwY0FGQ3FzWU9HNnVLcU4zTUhVd0RnWUQKVlIwUEFRSC9CQVFEQWdXZ01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFNQmdOVgpIUk1CQWY4RUFqQUFNQjBHQTFVZERnUVdCQlFnS01icnBUb3k4NVcvRy9hMGZtYzlDMUJRbURBWEJnTlZIUkVFCkVEQU9nZ3h0YjJWc2IzWmxMbWx1Wm04d0NnWUlLb1pJemowRUF3SURTQUF3UlFJZ1EzTzhJZ0N2MlRkNUhhV00KcE1LWmRCLzNXdEMreERlSVdPbER6L2hCdzE0Q0lRRExQNG0weFpmSkJvRGc5cERocThGdHN5VDdVZVhVdlZGQQpsS0tReFZNOXFBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= tls.key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUsyZjZHQlNZQ0R4eVoycnB2bVZ1YW5MNDhxeW9SK1NiWmxiQzNqSUZybzhvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVVNwQWM0YTVRdDBDQ1VraEZIZjdadm9HUVF5VU9RTFJSYWRtK0lFK1dWZDk4clpHOTRaaApvTzJsNlJaRjYydU83cWlnZWxpQnBjQUZDcXNZT0c2dUtnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo= kind: Secret metadata: creationTimestamp: "2022-10-19T07:24:26Z" name: moelove-tls namespace: default resourceVersion: "2103326" uid: 14f86514-a1d1-4d99-b000-9ed8b5189d56 type: kubernetes.io/tls </code></pre> <p>通过上述命令,在 Kubernetes 中创建了一个名为 <code>moelove-tls</code> 的 secret 资源,并且它是 <code>kubernetes.io/tls</code> 类型的。</p> <p>应用程序想要使用的时候,直接引用该资源即可从中获取相应的证书信息。多数情况下,我们会将其用于 Ingress 的场景中。比如:</p> <pre><code class="language-Bash">➜ ~ kubectl create ingress moelove-ing --rule="moelove.info/=moelove:8080,tls=moelove-tls" ingress.networking.k8s.io/moelove-ing created ➜ ~ kubectl get ing moelove-ing -oyaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: creationTimestamp: "2022-10-19T07:32:43Z" generation: 1 name: moelove-ing namespace: default resourceVersion: "2104268" uid: b90f09f7-8036-4b9f-9744-a247141ea8da spec: rules: - host: moelove.info http: paths: - backend: service: name: moelove port: number: 8080 path: / pathType: Exact tls: - hosts: - moelove.info secretName: moelove-tls status: loadBalancer: {} </code></pre> <p>通过上述命令,创建了一个名为 <code>moelove-ing</code> 的 Ingress 资源,并声明其域名为 <code>moelove.info</code> , 且使用 <code>moelove-tls</code> 为此域名添加了证书保护。对应的 Ingress controller 组件获取到此 Ingress 资源后,便可自动的将证书配置给此域名使用,进而提升网站的安全性。</p> <h3>遇到了哪些问题</h3> <p>我们来看看在此过程中遇到了哪些问题呢?</p> <h4>证书签发过程繁琐</h4> <p>上述内容中我并没有演示如何进行证书的签发,如果你对此感兴趣可以查看 <a href="https://www.openssl.org/docs/faq.html">OpenSSL 的文档</a> 。在证书的签发过程中,需要理解很多概念。而且签发过程都发生在 Kubernetes 集群之外,不能很好的通过“声明式”配置的方式来了解具体发生了什么事情。尤其是证书可以有多种不同的加密算法,各种不同的配置等。</p> <p>所以如果使用默认的方式,只能最后将生成的证书和密钥存储在 Kubernetes 的 Secrets 中。</p> <h4>证书续签/重签繁琐</h4> <p>我们都知道证书是有过期时间的,在证书过期或者被吊销之前,必须准备好新的证书,并且保证其过期时间要晚于原证书的过期时间。</p> <p>在 Kubernetes Secrets 中存储的证书,从这方面考虑的话有些不足:</p> <ul> <li> <p>不存在自动化的过期时间检查</p> <ul> <li>也就是说,你可以在 Kubernetes 中存储任意的证书,无论该证书是否已经过期。</li> </ul> </li> <li> <p>不存在无效数据的检查</p> <ul> <li>也就是说,如果存储在 Kubernetes Secrets 中的数据是损坏的,或者是无效的,那么在 Kubernetes 中也不会有什么特殊的处理。</li> </ul> </li> </ul> <h4>安全性不足</h4> <p>事实上,存储在 Kubernetes Secretes 中的证书和密钥信息,仅仅是进行了 base64 的编码,任何人只要拿到了此数据,均可对其进行 base64 的解码,进而获取到其中的真实数据。比如:</p> <pre><code class="language-Bash">➜ ~ kubectl get secrets moelove-tls -o jsonpath='{ .data.tls\.crt }' |base64 -d -----BEGIN CERTIFICATE----- MIICEzCCAbmgAwIBAgIUTxBL/ZBGi8BB9AU7bQZ/c+w6/TswCgYIKoZIzj0EAwIw TTELMAkGA1UEBhMCQ04xEDAOBgNVBAcTB0JlaWppbmcxFTATBgNVBAoTDE1vZUxv dmUgSU5GTzEVMBMGA1UEAxMMbW9lbG92ZS5pbmZvMB4XDTIyMTAxOTA3MTcwMFoX DTIzMTAxOTA3MTcwMFowTTELMAkGA1UEBhMCQ04xEDAOBgNVBAcTB0JlaWppbmcx FTATBgNVBAoTDE1vZUxvdmUgSU5GTzEVMBMGA1UEAxMMbW9lbG92ZS5pbmZvMFkw EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUSpAc4a5Qt0CCUkhFHf7ZvoGQQyUOQLR Radm+IE+WVd98rZG94ZhoO2l6RZF62uO7qigeliBpcAFCqsYOG6uKqN3MHUwDgYD VR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV HRMBAf8EAjAAMB0GA1UdDgQWBBQgKMbrpToy85W/G/a0fmc9C1BQmDAXBgNVHREE EDAOggxtb2Vsb3ZlLmluZm8wCgYIKoZIzj0EAwIDSAAwRQIgQ3O8IgCv2Td5HaWM pMKZdB/3WtC+xDeIWOlDz/hBw14CIQDLP4m0xZfJBoDg9pDhq8FtsyT7UeXUvVFA lKKQxVM9qA== -----END CERTIFICATE----- </code></pre> <p>通过以上命令便直接拿到了证书相关的原始数据。</p> <p>另一方面,当我们要更新证书和密钥数据的时候,并不存在二次确认的过程,可以直接进行更新。</p> <p>这对于大多数场景下都是不符合安全策略的。</p> <p>接下来我们看看 cert-manager 如何解决这些问题</p> <h3>cert-manager 如何解决</h3> <h4>自动签发</h4> <p>Cert-manager 通过 CRD 的方式进行开发和扩展,其添加和实现了 <code>Issuers</code> 和 <code>ClusterIssuers</code> 资源,代表证书的签发机构(CA)。</p> <p>并且支持了多种内置类型,以及可以很方便的与外部组件进行集成,比如:</p> <ul> <li> <p>SelfSigned:自签证书</p> </li> <li> <p>CA:提供 CA 进行签发</p> </li> <li> <p>Vault:使用 Vault 进行签发</p> </li> <li> <p>Venafi:使用 Venafi 进行签发</p> </li> <li> <p>External:使用一些外部的组件进行签发,比如:</p> <ul> <li><a href="https://github.com/Skyscanner/kms-issuer">kms-issuer</a>:使用 AWS KMS 签发</li> <li><a href="https://github.com/jetstack/google-cas-issuer">google-cas-issuer</a>:使用 Google CAS 进行签发</li> <li><a href="https://github.com/cloudflare/origin-ca-issuer">origin-ca-issuer</a>:使用 <a href="https://developers.cloudflare.com/ssl/origin-configuration/origin-ca">Cloudflare Origin CA</a> 进行签发</li> </ul> </li> <li> <p>ACME(Automated Certificate Management Environment ):自动化进行证书签发</p> </li> </ul> <ol> <li>通过这些组件可以非常方便的进行证书的签发。后续内容中将会以 Vault 为例进行具体介绍。</li> </ol> <h4>自动续签/重签</h4> <p>Cert-manager 中我们可以非常方便的通过 <code>cmctl</code> CTL 手动的进行证书 renew 操作,同时 cert-manager 也会自动的检查证书的有效期和证书的完整性。</p> <p>如果出现证书过期,或者证书数据不完整,都可以自动触发证书的重新签发,节约了人力成本和维护成本。</p> <h4>安全性保障</h4> <p>Cert-manager 中通过 CRD 的方式增加了 <code>signers</code> 资源,允许对证书请求进行确认,进行 <code>Approved</code> 或者 <code>Denied</code> 。只有 Approve 后,才会真正生效,并签发证书。这样可以更加的安全。</p> <h2>APISIX Ingress 如何与 cert-manager 集成</h2> <h3>安装部署</h3> <p>Apache APISIX Ingress 是一个 Kubernetes Ingress controller,可以支持通过 Ingress,自定义资源,以及 Gateway API 等方式进行代理规则的配置。</p> <p>接下来演示如何将 APISIX Ingress 与 cert-manager 集成,为代理的域名添加 TLS 证书,提升安全性。</p> <p>同时,我们使用 Vault 进行证书的签发。</p> <h4>部署 APISIX Ingress controller</h4> <p>部署 APISIX Ingress 很简单,仅需要执行如下步骤即可:</p> <pre><code class="language-Bash">tao@moelove:~$ helm repo add apisix https://charts.apiseven.com tao@moelove:~$ helm repo add bitnami https://charts.bitnami.com/bitnami tao@moelove:~$ helm repo update tao@moelove:~$ helm install apisix apisix/apisix --set gateway.tls.enabled=true --set gateway.type=NodePort --set ingress-controller.enabled=true --set ingress-controller.config.apisix.serviceNamespace=apisix --namespace apisix --create-namespace --set ingress-controller.config.apisix.serviceName=apisix-admin --set ingress-controller.config.ingressPublishService="apisix/apisix-gateway" NAME: apisix LAST DEPLOYED: Wed Oct 19 21:33:37 2022 NAMESPACE: apisix STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: 1. Get the application URL by running these commands: export NODE_PORT=$(kubectl get --namespace apisix -o jsonpath="{.spec.ports[0].nodePort}" services apisix-gateway) export NODE_IP=$(kubectl get nodes --namespace apisix -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT </code></pre> <p>待所有的 Pod 处于 Running 状态便表示已经部署成功。</p> <pre><code class="language-Bash">tao@moelove:~$ kubectl -n apisix get pods NAME READY STATUS RESTARTS AGE apisix-777c9fdd67-rf8zs 1/1 Running 0 6m48s apisix-etcd-0 1/1 Running 0 6m48s apisix-etcd-1 1/1 Running 0 6m48s apisix-etcd-2 1/1 Running 0 6m48s apisix-ingress-controller-568544b554-k7nd4 1/1 Running 0 6m48s </code></pre> <h4>部署 Vault</h4> <p>部署 Vault 的时候,也可以使用 Helm 。这里我增加了一个 <code>--set "server.dev.enabled=true"</code> 配置项,这样在部署后无需进额外操作便可直接使用了。 (注意这个配置不要用到生产环境)</p> <pre><code class="language-Bash">tao@moelove:~$ helm repo add hashicorp https://helm.releases.hashicorp.com tao@moelove:~$ helm install vault hashicorp/vault --set "injector.enabled=false" --set "server.dev.enabled=true" NAME: vault LAST DEPLOYED: Wed Oct 19 21:53:50 2022 NAMESPACE: default STATUS: deployed REVISION: 1 NOTES: Thank you for installing HashiCorp Vault! Now that you have deployed Vault, you should look over the docs on using Vault with Kubernetes available here: https://www.vaultproject.io/docs/ Your release is named vault. To learn more about the release, try: $ helm status vault $ helm get manifest vault </code></pre> <p>在部署完成后,当 Pod 处于 Running 状态时,说明已经部署完成。</p> <pre><code class="language-Bash">tao@moelove:~$ kubectl get pods NAME READY STATUS RESTARTS AGE vault-0 1/1 Running 0 29s tao@moelove:~$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 &lt;none&gt; 443/TCP 84m vault ClusterIP 10.96.190.88 &lt;none&gt; 8200/TCP,8201/TCP 4m14s vault-internal ClusterIP None &lt;none&gt; 8200/TCP,8201/TCP 4m14s </code></pre> <p>接下来进入 Vault 内进行操作,此处开启了 pki 的能力,并且配置了对应的 policy。</p> <pre><code class="language-Bash">tao@moelove:~$ kubectl exec -it vault-0 -- sh / $ vault secrets enable pki Success! Enabled the pki secrets engine at: pki/ / $ vault write pki/root/generate/internal common_name=moelove.info ttl=8760h Key Value --- ----- certificate -----BEGIN CERTIFICATE----- MIIDODCCAiCgAwIBAgIUds5uMJV9rOkwFEt6Xof5T2SVFccwDQYJKoZIhvcNAQEL ... VM4DRVgDkqY9JdHU -----END CERTIFICATE----- expiration 1668983612 issuer_id 8df13015-7c70-df9a-7bb7-9b3b4afe7f82 issuer_name n/a issuing_ca -----BEGIN CERTIFICATE----- MIIDODCCAiCgAwIBAgIUds5uMJV9rOkwFEt6Xof5T2SVFccwDQYJKoZIhvcNAQEL ... VM4DRVgDkqY9JdHU -----END CERTIFICATE----- key_id c9fcfcb0-3548-a9a7-e706-30510592c797 key_name n/a serial_number 76:ce:6e:30:95:7d:ac:e9:30:14:4b:7a:5e:87:f9:4f:64:95:15:c7 / $ / $ vault write pki/config/urls issuing_certificates="http://vault.default:8200/v1/pki/ca" crl_distribution_points="http://vault.default:8200/v1/pki/crl" Success! Data written to: pki/config/urls / $ vault write pki/roles/moelove-dot-info allowed_domains=moelove.info allow_subdomains=true max_ttl=72h Success! Data written to: pki/roles/moelove-dot-info / $ / $ vault policy write pki - &lt;&lt;EOF &gt; path "pki*" { capabilities = ["read", "list"] } &gt; path "pki/sign/moelove-dot-info" { capabilities = ["create", "update"] } &gt; path "pki/issue/moelove-dot-info" { capabilities = ["create"] } &gt; EOF Success! Uploaded policy: pki </code></pre> <p>接下来,配置 Kubernetes 认证:</p> <pre><code class="language-Bash">/ $ vault auth enable kubernetes Success! Enabled kubernetes auth method at: kubernetes/ / $ vault write auth/kubernetes/config kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" Success! Data written to: auth/kubernetes/config / $ vault write auth/kubernetes/role/issuer bound_service_account_names=issuer bound_service_account_namespaces=default policies=pki ttl=20m Success! Data written to: auth/kubernetes/role/issuer </code></pre> <p>完成上述操作后,接下来部署 cert-manager。</p> <h4>部署 cert-manager</h4> <p>现在可以通过 Helm 安装 cert-manager 了,安装的过程也比较简单。</p> <pre><code class="language-Bash">tao@moelove:~$ helm repo add jetstack https://charts.jetstack.io tao@moelove:~$ helm repo update jetstack Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "jetstack" chart repository Update Complete. ⎈Happy Helming!⎈ tao@moelove:~$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.10.0/cert-manager.crds.yaml customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created tao@moelove:~$ helm install \ &gt; cert-manager jetstack/cert-manager \ &gt; --namespace cert-manager \ &gt; --create-namespace \ &gt; --version v1.10.0 xNAME: cert-manager LAST DEPLOYED: Wed Oct 19 22:51:06 2022 NAMESPACE: cert-manager STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: cert-manager v1.10.0 has been deployed successfully! In order to begin issuing certificates, you will need to set up a ClusterIssuer or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer). More information on the different types of issuers and how to configure them can be found in our documentation: https://cert-manager.io/docs/configuration/ For information on how to configure cert-manager to automatically provision Certificates for Ingress resources, take a look at the `ingress-shim` documentation: https://cert-manager.io/docs/usage/ingress/ </code></pre> <p>接下来检查 Pod 的状态:</p> <pre><code class="language-Bash">tao@moelove:~$ kubectl -n cert-manager get pods NAME READY STATUS RESTARTS AGE cert-manager-69b456d85c-znpq4 1/1 Running 0 117s cert-manager-cainjector-5f44d58c4b-wcd27 1/1 Running 0 117s cert-manager-webhook-566bd88f7b-7rptf 1/1 Running 0 117s </code></pre> <p>接下来便可以开始配置和验证了。</p> <h3>如何配置及验证</h3> <h4>配置和签发证书</h4> <pre><code class="language-Bash">tao@moelove:~$ kubectl create serviceaccount issuer serviceaccount/issuer created tao@moelove:~$ kubectl get secret NAME TYPE DATA AGE sh.helm.release.v1.vault.v1 helm.sh/release.v1 1 36m tao@moelove:~$ vim issuer-secret.yaml tao@moelove:~$ cat issuer-secret.yaml apiVersion: v1 kind: Secret metadata: name: issuer-token-moelove annotations: kubernetes.io/service-account.name: issuer type: kubernetes.io/service-account-token tao@moelove:~$ kubectl apply -f issuer-secret.yaml secret/issuer-token-moelove created tao@moelove:~$ kubectl get sa,secret NAME SECRETS AGE serviceaccount/default 0 118m serviceaccount/issuer 0 2m11s serviceaccount/vault 0 38m NAME TYPE DATA AGE secret/issuer-token-moelove kubernetes.io/service-account-token 3 35s secret/sh.helm.release.v1.vault.v1 helm.sh/release.v1 1 38m </code></pre> <p><strong>创建 Issuer</strong></p> <p>通过此配置将使用 Vault 作为证书签发机构,通过引用在 Vault 中配置的 role 和 secret 等,进行自动的签发。</p> <pre><code class="language-Bash">tao@moelove:~$ cat vault-issuer.yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: vault-issuer namespace: default spec: vault: server: http://vault.default path: pki/sign/moelove-dot-info auth: kubernetes: mountPath: /v1/auth/kubernetes role: moelove-dot-info secretRef: name: issuer-token-moelove key: token tao@moelove:~$ kubectl apply -f vault-issuer.yaml issuer.cert-manager.io/vault-issuer created </code></pre> <p><strong>创建证书</strong></p> <p>通过此处的配置即可自动的签发证书,并在后续使用时候可以通过 <code>moelove-info-tls</code> 进行引用。</p> <pre><code class="language-Bash">tao@moelove:~$ cat moelove-dot-info-cert.yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: moelove-info namespace: default spec: secretName: moelove-info-tls issuerRef: name: vault-issuer commonName: www.moelove.info dnsNames: - www.moelove.info tao@moelove:~$ kubectl apply -f moelove-dot-info-cert.yaml certificate.cert-manager.io/moelove-info created </code></pre> <h4>验证</h4> <p>接下来通过代理一个 HTTPBIN 的服务进行验证。</p> <p>首先创建一个 HTTPBIN 的应用程序,并创建相应的 Service。</p> <pre><code class="language-Bash">kubectl run httpbin --image kennethreitz/httpbin kubectl expose pod httpbin --port=80 </code></pre> <p>然后定义如下资源进行代理和引用证书:</p> <pre><code class="language-YAML"># Define ApisixTls Objects apiVersion: apisix.apache.org/v2 kind: ApisixTls metadata: name: moelove spec: hosts: - moelove.info secret: name: moelove-info-tls --- # Define the route to access the backend apiVersion: apisix.apache.org/v2 kind: ApisixRoute metadata: name: moelove spec: http: - name: httpbin match: paths: - /* hosts: - moelove.info backends: - serviceName: httpbin servicePort: 80 </code></pre> <p>将这些资源应用到集群中即可。然后通过 <code>kubectl port-forward</code> 转发 APISIX 的 443 端口到本地后, 进行测试访问:</p> <pre><code class="language-Bash">$ ~ kubectl port-forward -n ingress-apisix svc/apisix-gateway 8443:443 &amp; $ ~ curl -sk https://moelove.info:8443/ip --resolve 'moelove.info:8443:127.0.0.1' { "origin": "172.17.18.1" } </code></pre> <p>可以看到,已经正确的为 <code>moelove.info</code> 这个域名配置了 HTTPS 证书,并且通过 APISIX Ingress 为其配置了代理。</p> <h2>总结</h2> <p>本文中介绍了 Kubernetes 中证书的默认存储方式,以及这种方式存在的一些痛点。cert-manager 的出现比较好的解决了这些问题,逐步成为了 Kubernetes 生态中证书签发/管理领域中的事实标准。并且其可以和 Vault 等工具进行集成,更加的安全。</p> <p>Apache APISIX Ingress 项目致力于打造更好用的 Ingress controller,所以很早就添加了完善的 cert-manager 集成能力。本篇中也通过理论加实践的方式为读者介绍了如何在 Apache APISIX Ingress 中配合使用 cert-manager 通过 Vault 签发的证书,并为应用程序提供 HTTPS 代理。希望对读者能有所帮助。</p>

Apache APISIX 如何与 gRPC 服务通信

<p>gRPC 是由 Google 开源的一个 RPC 框架,旨在统一服务间通信的方式。该框架基于 HTTP/2 协议传输,使用 Protocol Buffers 作为接口描述语言,可以自动生成服务间调用的代码。</p> <h2>为什么 gRPC 那么重要</h2> <p>由于 Google 在云原生和开发者关系上无与伦比的影响力,gRPC 逐渐成为了 RPC 框架的事实上标准。</p> <p>gRPC 的地位是如此强势,以至于你在设计时如果没有选择 gRPC 作为 RPC 的方式,你就必须给出为什么不选择 gRPC 的理由。</p> <p>不然总会有人问,为什么不选择主流的 gRPC 呢?甚至连曾经大力推广自己的 RPC 标准 Dubbo 的阿里巴巴,在最新版本的 Dubbo 3 中也大幅度修改了协议设计,改成了能同时兼容 gRPC 和 Dubbo 2 的 gRPC 变种。实际上,Dubbo 3 与其说是继承 Dubbo 2 的续作,不如说是对 gRPC 霸主地位的一个承认。</p> <p>许多提供了 gRPC 的服务,虽然也提供了对应的 HTTP 接口,但是由于这类接口往往只是出于兼容的目的,所以在实际体验上并不能与 gRPC 版本相媲美。</p> <p>一个常见的现状是,如果能通过 gRPC 方式接入,那么可以直接引入对应的 SDK;如果只能走普通的 HTTP 方式接入,那么通常会被引向一个文档页面,需要自行实现对应的 HTTP 操作。哪怕 HTTP 接入也可以通过 OpenAPI spec 等方式来生成对应的 SDK,但是由于优先级不高,很少会有开发者会像对待 gRPC 一样认真对待 HTTP 接入的使用者。</p> <h2>没有直接与 gRPC 服务通信所带来的限制</h2> <p>APISIX 采用 etcd 作为配置中心。etcd 从 v3 版本开始,就把接口迁移到了 gRPC 上。但是由于 OpenResty 生态里面没有对 gRPC 支持的项目,APISIX 就只能调用 etcd 的 HTTP 接口。</p> <p>etcd 的 HTTP 接口是通过 gRPC-gateway 提供的,本质上是 etcd 在服务端运行一个 HTTP to gRPC 的代理,外部的 HTTP 请求会转换成 gRPC 请求。在实践过程中,我们也发现了一些 HTTP API 跟 gRPC API 交互的问题。事实上,拥有 gRPC-gateway 并不意味着能够完美支持 HTTP 访问,这里还是有些细微的差别。</p> <p>我把过去几年来在 etcd 上遇到的相关问题列举了一下:</p> <ol> <li> <p>某些情况下,etcd 默认没有开启 gRPC-gateway。由于维护者的疏忽,在有些平台下,etcd 的默认配置中不会开启 gRPC-gateway。所以我们不得不在文档中增加了“检查当前 etcd 是否开启了 gRPC-gateway”的说明。详情可参考 <a href="https://github.com/apache/apisix/pull/2940">PR-2940</a>。</p> </li> <li> <p>gRPC 默认会限制响应最大不会超过 4MB。etcd 在它提供的 SDK 中把这个限制去掉了,但是忘记在 gRPC-gateway 里面去掉这个限制。结果导致官方的 etcdctl (基于它提供的 SDK 构建)能正常工作,但是 APISIX 却访问不到。详情可参考 <a href="https://github.com/etcd-io/etcd/issues/12576">issue-12576</a>。</p> </li> <li> <p>同样的问题也发生在同一连接的最大请求数上。Go 的 HTTP2 实现有一个 <code>MaxConcurrentStreams</code>的配置,控制单个客户端能同时发送的请求数,默认是 <code>250</code>。正常情况下,哪个客户端会同时发送超过 250 个请求呢?所以 etcd 一直沿用这一配置。然而 gRPC-gateway 这个代理所有 HTTP 请求到本机的 gRPC 接口的“客户端”,却有可能超出这一限制。详情可参考 <a href="https://github.com/etcd-io/etcd/issues/14185">issue-14185</a>。</p> </li> <li> <p>etcd 在开启 mTLS 后,会用同一个证书来作为 gRPC-gateway 的服务端证书,兼 gRPC-gateway 访问 gRPC 接口时的客户端证书。如果该证书上启用了 server auth 的拓展,但是没有启用 client auth 的拓展,那么会导致证书检验出错。直接用 etcdctl 访问不会出现这个问题,因为该证书在这种情况下不会作为客户端证书使用。详情可参考 <a href="https://github.com/etcd-io/etcd/issues/9785">issue-9785</a>。</p> </li> <li> <p>etcd 在开启 mTLS 后,允许针对证书里面的用户信息配置安全策略。如上所述,gRPC-gateway 在访问 gRPC 接口时用的是固定的一个客户端证书,而非一开始访问 HTTP 接口所用的证书信息。这个功能自然就没办法正确工作了。详情可参考 <a href="https://github.com/apache/apisix/issues/5608">issue-5608</a>。</p> </li> </ol> <p>导致以上问题出现的原因,可以归类为两点:</p> <ul> <li> <p>gRPC-gateway(或许包括其他试图把 HTTP 转成 gRPC 的方式)并非灵丹妙药;</p> </li> <li> <p>etcd 的开发者对 HTTP 转 gRPC 这一路径关注度不够。而且他们的最大用户——Kubernetes 不会用到这个功能。</p> </li> </ul> <p>当然,我在这里并不是讨论特定软件的问题。etcd 只是个典型的 gRPC 服务。事实上,把 gRPC 作为一等公民的服务,在对 HTTP 的支持上或多或少都有点类似的限制。</p> <h2>APISIX 3.0 是如何解决上述问题</h2> <p>常言道,“如果山不会向你走来,那么你就走向山”。我们如果实现一个在 OpenResty 下可用的 gRPC 客户端,那么就能直接跟 gRPC 服务通信了。</p> <p>出于工作量和成熟稳定的考虑,我们决定基于常用的 gRPC 库进行二次开发,而不是自己从头写一个。这个过程中主要考察了以下的 gRPC 库:</p> <ul> <li> <p>NGINX 的 gRPC 功能。NGINX 并没有把 gRPC 的能力暴露给外部用户,甚至连高层次的接口也没有。如果要用,只能自己复制几个底层的函数,然后整合成高层次的接口。这违背了我们的目标“工作量和成熟稳定”。</p> </li> <li> <p>官方 C++ 的 gRPC 库。由于我们的构建体系是基于 NGINX 的,要想整合 C++ 的库会有点复杂。另外该库的依赖接近 2GB,总对于 APISIX 的构建是一大挑战。</p> </li> <li> <p>gRPC 的官方 Go 实现。得益于 Go 强大的工具链,我们可以很方便地把项目构建起来里面来。可惜 Go 的性能离 C++ 的版本差得太远。我们考察了另外一个 Go 实现 <a href="https://github.com/bufbuild/connect-go/">Connect</a>。但是该项目的性能并不比官方版本强。</p> </li> <li> <p>Rust 实现的 gRPC 库。应该是结合依赖管理和性能的天作之选,可惜我们不熟悉 Rust,所以不敢在这上面下注。</p> </li> </ul> <p>考虑到作为 gRPC Client 基本上的操作都是 IO Bound,所以对性能要求不高,所以我们最后选择了基于 go-grpc 来实现。</p> <h3>实现过程</h3> <p>为了能够跟 Lua 的协程调度打通,我们编写了一个 <a href="https://github.com/api7/grpc-client-nginx-module">NGINX C module</a>。一开始,我们是想把 Go 代码通过 cgo 编译成静态链接库的方式集成到这个 C module 当中,但是发现由于 Go 是多线程应用,而在 fork 之后,子进程并不会继承父进程的所有线程,没办法适应 NGINX 的 master-worker 多进程架构。所以我们选择把 Go 代码编译成动态链接库,然后在运行时再加载到 Worker 进程进来。</p> <p>为了能把 Go 的协程和 Lua 的协程协调起来,我们实现了一个任务队列机制。当 Lua 代码发起 gRPC 的 IO 操作时,它会提交一个任务到 Go 一侧,然后把自己挂起来。这个任务会由一个 Go 协程执行,执行结果写入到队列中。NGINX 一侧有个 background thread 会消费任务执行结果,并重新调度对应的 Lua 协程,继续执行 Lua 代码。这么一来,gRPC IO 操作,在 Lua 代码眼里,跟普通的 socket 操作并无二致。</p> <p>目前,该 <a href="https://github.com/api7/grpc-client-nginx-module">NGINX C module</a> 已大体完工。现在我们只需要把 etcd 的 <code>.proto</code> 文件(该文件定义了它的 gRPC 接口)拿出来修改一下,然后在 Lua 代码里面加载该文件,就得到了如下的 etcd 客户端:</p> <pre><code class="language-lua">local gcli = require("resty.grpc") assert(gcli.load("t/testdata/rpc.proto")) local conn = assert(gcli.connect("127.0.0.1:2379")) local st, err = conn:new_server_stream("etcdserverpb.Watch", "Watch", {create_request = {key = ngx.var.arg_key}}, {timeout = 30000}) if not st then ngx.status = 503 ngx.say(err) return end for i = 1, (ngx.var.arg_count or 10) do local res, err = st:recv() ngx.log(ngx.WARN, "received ", cjson.encode(res)) if not res then ngx.status = 503 ngx.say(err) break end end </code></pre> <p>当下,我们跟 etcd HTTP 接口对接的客户端项目 <a href="https://github.com/api7/lua-resty-etcd">lua-resty-etcd</a>,光 Lua 代码就有 1600 行。这么一对比,目前的基于 gRPC 实现可以说是工程上的奇迹了。</p> <p>当然,目前我们离替换 <code>lua-resty-etcd</code> 还有一段不短的距离。要想完备地对接 etcd,<code>grpc-client-nginx-module</code> 还需要补全以下的功能:</p> <ul> <li> <p>支持 mTLS;</p> </li> <li> <p>支持设置 gRPC metadata;</p> </li> <li> <p>支持设置诸如 <code>MaxConcurrentStreams</code> 和 <code>MaxRecvMsgSize</code> 等参数;</p> </li> <li> <p>支持在 L4 下调用。</p> </li> </ul> <p>好在我们已经搭好了架子,支持这些也不过是水到渠成的事情。</p> <p><code>grpc-client-nginx-module</code> 将会整合到 APISIX 3.0 版本中,届时 APISIX 开发者可以在 APISIX 的插件代码中使用该模块的方法,跟 gRPC 服务直接通信。</p> <p>有了对 gRPC 的原生支持,APISIX 除了可以得到更好的 etcd 体验外,还为诸如 gRPC health check 、基于 gRPC 的 OpenTelemetry 数据上报等功能打开了可能性的大门。相信在不久的将来,我们会看到 APISIX 基于 gRPC 的更多用法!</p>

为什么 Apache APISIX 是最好的 API 网关?

<p>今天,我们可以通过手机和各种 APP 完成各种各样的事情,比如社交,网购等。这些行为的背后,API 起到了关键的作用。作为 API 的使用者,我们并不关心 API 的稳定、安全和高效,但是通过 API 提供数据服务的企业则需要选择一个合适的 API 网关,用来保证数千乃至数万的 API 为提供快速和安全的服务。</p> <p>在 <a href="https://landscape.cncf.io/card-mode?category=api-gateway&amp;grouping=category&amp;sort=contributors">CNCF 的 API Gateway landscape</a> 中有接近 20 个 API 网关的选型(不包括公有云厂商的产品),包括 Apache APISIX、Kong、Tyk 等等。很多网关都称自己是下一代 API 网关,是最受欢迎的开源 API 网关项目,那么事实如何呢?本文将通过开发者、知识产权和品牌、技术、生态等多个维度来看看 Apache APISIX 为什么是下一代 API 网关?</p> <p><img src="https://static.apiseven.com/2022/09/13/632052e8c6f34.png" alt="1.png" referrerpolicy="no-referrer"></p> <h2>工程师的眼睛是雪亮的</h2> <p>工程师是 API 和 API 网关的使用者和开发者,更多工程师关注和参与的 API 网关项目,代表的就是技术的趋势。我们从 GitHub 代码贡献者的维度,选取了 4 个开源 API 网关进行对比:Apache APISIX、Kong、Tyk 和 Gloo。</p> <p><img src="https://static.apiseven.com/2022/09/13/632055a37ac26.png" alt="2.png" referrerpolicy="no-referrer"></p> <p>Kong 和 Tyk 都是 2015 年之前开始研发的,Apache APISIX 和基于 Envoy 的 Gloo 是在 2019 年左右开始研发的。从上图中的 <a href="https://git-contributor.com/?chart=contributorOverTime&amp;repo=apache/apisix,solo-io/gloo,kong/kong,tyktechnologies/tyk">Github Contributor Over Time</a> 可以看出,Kong 经过近 7 年的发展已经有超过 250 个贡献者参与,远远超过了 Tyk 和 Gloo;而最年轻的 Apache APISIX,已经超过 320 个贡献者,数量和增速都远超 Kong,成为最多开发者参与贡献的 API 网关项目。</p> <p>除了贡献者总数外,还有一个指标可以看到更深层次的数据:贡献者的月度活跃。下图展示了以上四个开源 API 网关的<a href="https://git-contributor.com/?chart=contributorMonthlyActivity&amp;repo=apache/apisix,solo-io/gloo,kong/kong,tyktechnologies/tyk">月度活跃的开发者数量</a>:</p> <p><img src="https://static.apiseven.com/2022/09/13/63205698e44f5.png" alt="3.png" referrerpolicy="no-referrer"></p> <p>Tyk 的贡献者月活跃人数,长期都是在 5 个人左右,很少超过 10 个人;而 Kong 和 Gloo 的贡献者月活跃人数均在 10 到 20 之间浮动;而 Apache APISIX 基本保持在 20 人以上,最高接近 40 人,是开发者最活跃的 API 网关。</p> <p>这四个开源 API 网关项目背后,都是有对应的商业化公司的,所以还有第三个指标数据,那就是开源项目贡献者和商业公司员工数的对比:</p> <table> <thead> <tr> <th style="text-align:center"><strong>API Gateway</strong></th> <th style="text-align:center"><strong>APISIX</strong></th> <th style="text-align:center"><strong>Kong</strong></th> <th style="text-align:center"><strong>Tyk</strong></th> <th style="text-align:center"><strong>Gloo</strong></th> </tr> </thead> <tbody> <tr> <td style="text-align:center">monthly active</td> <td style="text-align:center">38</td> <td style="text-align:center">20</td> <td style="text-align:center">8</td> <td style="text-align:center">24</td> </tr> <tr> <td style="text-align:center">employees(from Linkedln)</td> <td style="text-align:center">40+</td> <td style="text-align:center">500+</td> <td style="text-align:center">200+</td> <td style="text-align:center">100+</td> </tr> </tbody> </table> <p>当前(2022 年第三季度)Kong 和 Tyk 的开源投入比例(月度活跃贡献者/员工总数)是 4%,Gloo 是 24%,APISIX 则是接近 100%。即使回到 2017 年,Kong 的月度活跃贡献者也在 5 个人左右。</p> <p>显而易见,开源项目 Apache APISIX 和它背后的商业化公司 API7.ai 从创建伊始,就保持了对于开源项目的持续投入,赢得了众多开发者的喜爱。</p> <h2>开源协议:企业用户选择开源项目的首要考虑因素</h2> <p>自从 MongoDB 修改了开源项目的 License 之后,在做基础软件的选型时,开源项目的 License 风险就是企业用户首要考虑的因素。</p> <p>从表面上看,Apache APISIX、Kong 和 Gloo 使用的都是商业友好的 Apache License Version 2.0,Tyk 使用的是带有传染性的 Mozilla Public License Version 2.0。</p> <p>更深入的看,Kong、Gloo 和 Tyk 这三个都是完全由商业公司控制的开源项目,它们可以像 MongoDB 一样随时在新版本中修改 License,限制公有云或者其他公司免费使用,进而要求你从开源用户转为付费客户来继续使用最新版本。</p> <p>没有人知道这样的事情是否会发生,以及发生的几率有多大。这种风险,就像达摩克利斯之剑一样,悬挂在企业用户的头上。</p> <p>在这种情况下,使用 Apache 软件基金会或者 CNCF 的开源项目是最明智的。而 Apache APISIX 是全球最大的软件基金会 -- Apache 软件基金会的顶级项目,所有的代码和品牌都归属于 Apache 软件基金会,任何商业公司都不能控制 Apache APISIX 项目,也不可能修改开源项目的 license。企业用户可以一直放心的使用,而不用担心收到律师和合规部门的问询邮件。</p> <h2>性能测试对比</h2> <p>在社区中经常会有用户提问:基于 Envoy 的 Gloo,和基于 NGINX 的 APISIX,谁的性能更胜一筹?</p> <p>虽然性能并不是选型中最重要的指标,但它确实是最直接的指标。下表的表格是 Apache APISIX 和 Gloo 的 Benchmark 结果。从表格中可以看到,Apache APISIX 的 QPS 是 Gloo 的 4.6 倍,同时 Apache APISIX 的延迟还不到 Gloo 的 7%。</p> <table> <thead> <tr> <th style="text-align:center"><strong>API Gateway</strong></th> <th style="text-align:center">Apache APISIX</th> <th style="text-align:center">Gloo Edge</th> </tr> </thead> <tbody> <tr> <td style="text-align:center"><strong>QPS</strong></td> <td style="text-align:center">59122</td> <td style="text-align:center">12903</td> </tr> <tr> <td style="text-align:center"><strong>Latency</strong></td> <td style="text-align:center">50.000% 470.00us<br>75.000% 648.00us<br>90.000% 0.89ms<br>99.000% 1.60ms</td> <td style="text-align:center">50.000% 6.80ms<br>75.000% 9.25ms<br>90.000% 11.32ms<br>99.000% 17.06ms</td> </tr> </tbody> </table> <p>这并不仅仅是 NGINX 和 Envoy 之间的差异造成的,而是因为 APISIX 在底层做了大量的优化,所以同样基于 NGINX 的 APSIX 相对于 Kong 也有巨大的性能优势。</p> <p><img src="https://static.apiseven.com/2022/09/13/6320574ba0244.png" alt="Performance" referrerpolicy="no-referrer"></p> <p>为什么 APISIX 的性能优势如何之大?在代码面前,没有任何秘密。让我们从技术的角度来详细分析下。</p> <h2>APISIX 的技术优势</h2> <p>以下内容是 Apache APISIX 和 Kong、Gloo 相比在技术方面的主要优势,大部分都是在底层模块上的优化和创新。在简单的 PoC 上并不一定能够体现出这些技术的优势,但在复杂的生产环境中,Apache APISIX 的这些优势将会造成巨大的差距。</p> <h3>无数据库依赖</h3> <p>在 APISIX 项目出现之前,也有非常多的商业 API 网关或开源 API 网关产品,但这些产品大多数都把 API 数据、路由、证书和配置等信息存放在一个关系型数据库中。</p> <p>将这些数据存储在关系型数据库的优势非常明显,用户可以更加方便地使用 SQL 语句进行灵活查询,也方便用户进行备份及后续维护。</p> <p>但是网关作为一个基础中间件,它处理了所有来自客户端的流量,这种情况下对于可用性的要求便会非常高。如果你的 API 网关依赖了一个关系型数据库,也就意味着关系型数据库一旦出现了故障(比如宕机、丢失数据),API 网关也会因此受到影响,整个业务系统的可用性也会大打折扣。</p> <p>而 APISIX 在设计之初,就从底层架构上避免了宕机、丢失数据等情况的发生。因为在控制面上,APISIX 使用了 etcd 存储配置信息,而不是使用关系型数据库,这样做的好处主要有以下几点:</p> <ol> <li>与产品架构的云原生技术体系更统一;</li> <li>更贴合 API 网关存放的数据类型;</li> <li>能更好地体现高可用特性;</li> <li>拥有低于毫秒级别的变化通知。</li> </ol> <p>使用 etcd 存储配置信息后,对于数据面而言只需监听 etcd 的变化即可。如果采用轮询数据库的方式,可能需要 5-10 秒才能获取到最新的配置信息;如果监听 etcd 的配置信息变更,APISIX 就可以将获取最新配置的时间控制在毫秒级别之内,达到实时生效。</p> <p>因此使用 etcd 作为存储,不仅让 APISIX 在底层上更加贴合云原生,也让它在系统高可用的体现上带来了更多优势。</p> <h3>高性能路由匹配算法</h3> <p>API 网关需要从每个请求的 Host、URI、HTTP 方法等特征中匹配到目标规则,以决定如何对该请求进行处理,因此一个优秀的匹配算法是必不可少的。Hash 算法性能不错,但无法实现模糊匹配;正则可以模糊匹配,但性能不好,因此 Apache APISIX 选择使用树这样一种高效且支持模糊匹配的搜索数据结构。准确一些,Apache APISIX 使用的是 RadixTree,它提供了 KV 存储查找的数据结构并对只有一个子节点的中间节点进行了压缩,因此它又被称为压缩前缀树。此外,在已知 API 网关产品中 Apache APISIX 首次将 RadixTree 应用到了路由匹配中,支持一个前缀下有多个不同路由的场景,具体实现见 <a href="https://github.com/api7/lua-resty-radixtree"><code>lua-resty-radixtree</code></a>。</p> <p>当对某个请求进行匹配时,RadixTree 将采用层层递进的方式进行匹配,其复杂度为 O(K)(K 是路由中 URI 的长度,与 API 数量多少无关),该算法非常适合公有云、CDN 以及路由数量比较多的场景,可以很好地满足路由数量快速增长的需求。</p> <h3>高性能 IP 匹配算法</h3> <p>IP 地址有 2 种记法:标准 IP 表示方法与 CIDR 表示方法,以 32 位的 IPv4 为例:</p> <ul> <li>标准 IP 记法:192.168.1.1</li> <li>CIDR 记法:192.168.1.1/8</li> </ul> <p>Apache APISIX 的 IP 匹配算法与路由匹配算法所使用的原理以及原始数据是不一样的。以 192.168.1.1 这个 IP 为例,由于每个 IP 段的范围是 0 到 255,因此在对 IP 进行匹配时我们可以认为 IP 地址是由 4 个 16 位整数型的数构成的,IP 长度是固定的。那么我们可以采用更高效的算法完成匹配。</p> <p>假设现在有一个包含 500 条 IPv4 记录的 IP 库,APISIX 会将 500 条 IPv4 的记录缓存在 Hash 表中,当进行 IP 匹配时使用 Hash 的方式进行查找,时间复杂度为 O(1)。而其他 API 网关则是通过遍历的方式完成 IP 匹配,发送到网关每个请求将逐个遍历最多 500 次是否相等后才能知道计算结果。所以 APISIX 的 高精度 IP 匹配算法大大提高了需要进行海量 IP 黑白名单匹配场景(如 WAF)的效率。</p> <h3>精细化路由</h3> <p>API 网关通过请求中的流量特征完成预设规则的匹配,常见特征包含了请求中的 Host、URI 路径、URI 查询参数、URI 路径参数、HTTP 请求方法、请求头等,这些特征是大部分 API 网关产品所支持的。相较于其它产品,Apache APISIX 支持了更多特征以解决复杂多变的使用场景。</p> <p>首先,Apache APISIX 支持 NGINX 内置变量,意味着我们可以将诸如 <code>uri</code>、<code>server_name</code>、<code>server_addr</code>、<code>request_uri</code>、<code>remote_port</code>、<code>remote_addr</code>、<code>query_string</code>、<code>host</code>、<code>hostname</code>、<code>arg_name</code> 等数十种 Nginx 内置变量作为匹配参数,以支持更复杂多变的匹配场景。NGINX 内置变量列表请参考 <a href="http://nginx.org/en/docs/varindex.html">NGINX 变量</a>。</p> <p>其次,Apache APISIX 支持将条件表达式作为匹配规则,其结构是 <code>[var, operator, val], ...]]</code>,其中:</p> <ul> <li><code>var</code> 值可使用 Nginx 内置变量;</li> <li><code>operator</code> 支持相等、不等、大于、小于、正则、包含等操作符。</li> </ul> <p>假设表达式为 <code>["arg_name", "==", "json"]</code>,它意味着当前请求的 URI 查询参数中,是否有一个为 <code>name</code> 的参数值等于 <code>json</code>。Apache APISIX 是通过自研的库 <code>lua-resty-expr</code> 实现该能力的,具体请参考 <a href="https://github.com/api7/lua-resty-expr">lua-resty-expr</a>。该特性将选择权交给了用户,可扩展性强。</p> <p>此外,Apache APISIX 支持设置路由 <code>ttl</code> 存活时间:</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/routes/2?ttl=60 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "uri": "/aa/index.html", "upstream": { "type": "roundrobin", "nodes": { "39.97.63.215:80": 1 } } }' </code></pre> <p>以上配置表示,在 60s 后 APISIX 会自动删除该路由配置,非常适合一些临时验证的场景,比如金丝雀发布、监控输出等。对于线上的流量分流非常方便,是其它网关产品所不具备的能力。</p> <p>最后一点是 Apache APISIX 支持自定义过滤函数,你可以通过在 <code>filter_func</code> 参数中编写自定义 Lua 函数,例如:</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "uri": "/index.html", "hosts": ["foo.com", "*.bar.com"], "filter_func": "function(vars) return vars['host'] == 'api7.ai' end", "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } } }' </code></pre> <p>其中 <code>filter_func</code> 入参是 <code>vars</code>,可从 <code>vars</code> 获取 Nginx 变量,然后实现自定义过滤逻辑。</p> <h3>支持多语言插件</h3> <p>虽然 API 网关、数据库或其他中间件都属于基础组件,但是更多时候用户是根据使用场景对 API 网关进行一些定制化地开发和系统集成。</p> <p>APISIX 目前已经支持了 80 多种插件,但仍然难以涵盖用户的所有使用场景。在实际使用场景中,很多企业都会针对具体业务进行定制化的插件开发,通过网关去集成更多的协议或者系统,最终在网关层实现统一管理。</p> <p>在 APISIX 早期版本中,开发者仅能使用 Lua 语言开发插件。虽然通过原生计算语言开发的插件具备非常高的性能,但是学习 Lua 这门新的开发语言是需要时间和理解成本的。</p> <p>针对这种情况,APISIX 提供了两种方式来解决:</p> <p>第一种方式是通过 Plugin Runner 来支持更多的主流编程语言(比如 Java、Python、Go 等等)。通过这样的方式,可以让后端工程师通过本地 RPC 通信,使用熟悉的编程语言开发 APISIX 的插件。</p> <p>这样做的好处是减少了开发成本,提高了开发效率,但是在性能上会有一些损失。那么,有没有一种既能达到 Lua 原生性能,同时又兼顾高级编程语言的开发效率方案呢?</p> <p><img src="https://static.apiseven.com/2022/09/13/632057a0ad122.png" alt="Multi-Language Architecture" referrerpolicy="no-referrer"></p> <p>第二种方式是使用 Wasm 开发插件,也就是上图左侧部分。Wasm(WebAssembly) 最早是用在前端和浏览器上的一个技术,但是现在在服务端它也逐渐展示出来它的优势。我们将 Wasm 嵌入到了 APISIX 中,用户就可以使用 Wasm 去编译成 Wasm 的字节码在 APISIX 中运行。最终达到的效果就是利用高效率,开发出了一个既有高性能又使用高级计算语言编写的 APISIX 插件。</p> <p>因此,在当前 APISIX 版本中,用户可以使用 Lua、Go、Python 和 Wasm 等多种方式,基于 APISIX 编写自定义插件。通过这样的方式,降低了开发者的使用门槛,也为 APISIX 的功能提供了更多的可能性。</p> <h2>总结</h2> <p>本文从工程师、开源协议、性能评测、技术、生态系统等多个角度分析和比较了一些 API 网关产品,可以看出 Apache APISIX 是其中的佼佼者,引领了 API 网关领域的创新。</p> <p>Apache APISIX 不仅是一个可以处理南北向流量的 API 网关,同时也有 APISIX Ingress Controller、Service Mesh 等开源产品。同时也提供了基于 APISIX 的企业级产品和 SaaS 产品。</p> <!-- markdown-link-check-disable --> <p>尝试 Apache APISIX 和 <a href="https://api7.ai/request-demo/">API7 企业产品</a>,就从今天开始!</p> <!-- markdown-link-check-enable -->

如何借助技术演进的机遇和挑战,助力企业数字化转型

<p>云原生在当下技术环境中越来越流行。关于云原生的概念,CNCF 给予了它如下定义:</p> <ul> <li>构筑在现代化的动态环境之上,也就是云上环境;</li> <li>基础技术为容器化技术,包含像服务网格、不可变基础设施以及声明式 API 等;</li> <li>主要特性包括弹性伸缩、可管理、可观测、自动化以及频繁变更等;</li> </ul> <p>回归到企业角度,在企业的发展过程中,为什么都在拥抱云原生,云原生对企业而言究竟有什么意义呢?</p> <p>从技术角度来看,云原生技术成为了一个技术趋势。截止到目前,无论是 K8s、容器化或者是云原生,这些技术的热度都在持续增加。根据 CNCF 2021 年的调查报告显示,Kubernetes 社区中有 62000 多名贡献者,贡献者数量非常庞大。当下的技术趋势下,越来越多的国内外公司都开始将更多的成本投入到云原生当中,提早加入赛道进行积极部署。</p> <p>从企业角度来看,云原生为企业带来了更多价值。云原生技术是以 Kubernetes 为基石,将 K8s 提供的一些特性进行了全方位呈现。比如通过容器化方式部署,提供了更为简单的弹性伸缩和故障自愈能力。方便按照业务需求在不同负载状态下进行弹性扩缩容,从而节约成本,将每台机器的性能发挥到最大化。</p> <p>此外在云原生环境中,通过统一的声明式进行基础设施配置时,更方便实现多平台的无缝迁移,尤其是迁移到通过 Kubernetes 一致性认证的平台当中。由于目前的云原生技术大都是一些开源项目,因此也大大避免了项目被厂商锁定的问题。</p> <h2>云原生带来的技术优势</h2> <p>云原生之所以开始流行,在技术层面必然有其优势。目前的云原生技术主要有两个方面,一个是以 Docker 为首的容器化技术,另外一个是以 Kubernetes 为首的容器编排技术。</p> <p>Docker 为技术世界引入了容器镜像,使容器镜像成为了一个标准化的交付单元。实际上,在 Docker 之前,容器化技术就已经存在,过早的技术我们不提,聊一个比较近的,比如 2008 年的 LXC(<a href="https://linuxcontainers.org/">Linux Containers</a>)。相比 Docker,LXC 之所以没有流行起来就是因为 Docker 提供了容器镜像,可以更加的进行标准化和迁移。并同时打造了 DockerHub 的公共服务,成为目前全球规模最大的容器镜像仓库。此外,通过容器化技术还可以实现一定程度的资源隔离。不仅实现 CPU、内存等资源隔离,也可以进行网络堆栈的隔离,从而更方便地在同一台机器上部署应用程序的多个副本。</p> <p>Kubernetes 实际上也是因为 Docker 容器化技术火热之后才逐步流行起来的。</p> <p>以 Kubernetes 为首的容器编排技术,主要提供了几个重要的能力。比如故障自愈、资源调度和服务编排。Kubernetes 内置了基于 DNS 的服务发现机制,得益于它的调度架构,可以非常方便地进行扩展,实现服务编排。</p> <p>目前越来越多的公司都在积极拥抱 Kubernetes,并将自己的应用转变为使用 Kubernetes 的方式部署。而作为云原生技术的基石,我们现在所聊的云原生技术,实际上都是以 Kubernetes 为大前提进行的。</p> <p><img src="https://static.apiseven.com/2022/09/26/6331203d3668c.png" alt="Kubernetes Cluster" referrerpolicy="no-referrer"></p> <h3>容器化优势</h3> <ol> <li>标准化交付</li> </ol> <p>容器镜像目前已经成为了一个标准化的交付单元。使用容器化技术,用户可以不再需要去交付源代码或者二进制,而是直接通过一个容器镜像的方式完成交付。依靠容器镜像的打包机制,可以在任意一个容器运行时当中,使用相同的镜像去启动一个服务,并产生相同的行为。</p> <ol start="2"> <li>可移植与轻量级,节约成本</li> </ol> <p>容器化技术通过使用 Linux 内核所提供的能力,可达到一定的隔离性,进而在迁移时更加方便。同时容器化技术是直接运行应用程序,与虚拟化技术相比,在技术实现上更加轻量,无需虚拟机中的 OS,</p> <p>所有的应用程序都可以共享内核。这样不仅可以节约成本,而且随着应用规模的增大,节约的成本也越多。</p> <ol start="3"> <li>方便资源管理</li> </ol> <p>在启动容器时,可以去为容器服务设置可以使用的 CPU、内存或者磁盘 IO 等属性。这样通过容器的方式去启动应用实例时,可以更好地进行规划与部署资源。</p> <h3>容器编排优势</h3> <ol> <li>简化工作流程</li> </ol> <p>在 Kubernetes 中,应用程序的部署相对 Docker 部署方式更容易进行管理。因为 Kubernetes 当中采用了声明式配置方式,比如用户在一个配置文件中,只要去声明这个应用程序它要使用的容器镜像是什么,以及暴露出来的服务端口是哪些就可以了,不需要额外管理这部分内容。只需根据声明式配置来完成相应操作,进而大大简化了工作流程。</p> <ol start="2"> <li>提升效率,节约成本</li> </ol> <p>此外 Kubernetes 的一个优势功能就是故障转移。当 Kubernetes 中的一个 node 节点挂掉后,Kubernetes 会将这些应用程序自动调度到其他正常的节点上并正常运行起来。 整个恢复过程并不需要人为干预及操作,所以在操作层面不仅提升了运维效率,还节约了时间成本。</p> <p>通过 Docker 和 Kubernetes 的崛起,你会发现,他们的出现给应用交付带来了巨大的革新与机遇。容器镜像作为标准的交付单元,缩短了交付流程,使其更容易与 CI/CD 系统集成。</p> <p>考虑到应用交付变得更快,那应用架构又是如何跟进云原生趋势的呢?</p> <h2>应用架构的演进</h2> <p>应用架构演进的起点,依旧是从单体架构说起。单体架构下所有的应用程序都耦合在一起,随着应用规模和需求的增加,单体架构不再满足团队协作开发的需求,便开始逐步引入分布式架构。</p> <p>在分布式架构中,最流行的就是微服务架构。微服务架构可以将服务拆分为多个模块,模块之间彼此进行通信、完成服务注册发现,实现限流和熔断等通用能力。</p> <p>此外在微服务架构中,还包含了多种模式。比如每服务数据库模式,它代表着每一个微服务都有一个各自的数据库,这种模式可以避免数据库层面对应用程序造成影响,但可能会引入更多的数据库实例。</p> <p>还有一种就是 API Gateway 模式。通过一个网关,接收集群或整个微服务架构的入口流量,通过 API 来完成流量分发。这也是目前使用较多的一个模式,像 Spring Cloud Gateway 或 Apache APISIX 这些网关产品都可以应用其中。</p> <p>而目前流行的架构已经不再是这种微服务架构了,开始逐步向云原生架构进行扩展。那在云原生下的微服务架构,是否可以简单地将原有的微服务构建成容器镜像,直接迁移到 Kubernetes 当中呢?</p> <p>理论上看起来可以,但在实际操作过程中会有一些挑战。因为在云原生的微服务架构中,这些组件不仅仅需要简单地通过容器方式运行,实际上还包括了服务的注册发现,以及服务配置等其他环节。</p> <p>同时在迁移的过程中,还会涉及到业务层面的改造与适配,就需要将认证、授权等通用逻辑和可观测性相关能力(日志、监控等)全部迁移到 K8s 中。所以从原先的物理机环境部署迁移至 K8s 平台当中时,情况会比实际复杂的多。</p> <p>在这种情况下,我们可以通过 Sidecar 模型来完成一个抽象,将上述场景简单化处理。</p> <p>通常情况下,Sidecar 模型以 Sidecar Proxy 的形式出现。通过将一些通用能力(如认证、授权和安全等)下沉到 Sidecar 中,进而从下图左侧演变为右侧模式。从图中可以看到,这种模式的调整,将原先需要维护多个组件,变成了只需要去维护两个东西(应用程序 +Sidecar)。同时在 Sidecar 模型自身中,包含一些通用组件,所以也不需要业务方自己去维护,因此可以很方便地解决微服务通信之间的问题。</p> <p><img src="https://static.apiseven.com/2022/09/26/633120929e005.png" alt="Sidecar 模型" referrerpolicy="no-referrer"></p> <p>为了避免每个微服务引入一个 Sidecar 时,出现单独配置的复杂场面和重复造轮子的情况,就可以通过引入控制面或者通过控制面注入的方式执行,这逐步形成了现在的服务网格(Service Mesh)。</p> <p>服务网格通常情况下需要两个组件,即控制面 + 数据面。控制面完成配置的下发和相关逻辑的执行,比如当前最为流行的 Istio;在数据上,你可以选择类似 Apache APISIX 这种 API 网关进行流量的转发与服务通信。得益于 APISIX 的高性能和可扩展,还可以进行一些定制化需求和自定义逻辑。如下展示了以 Istio+APISIX 的服务网格方案架构。</p> <p><img src="https://static.apiseven.com/2022/09/26/633120bc56a06.png" alt="服务网格方案架构" referrerpolicy="no-referrer"></p> <p>这套方案的优势在于,当你想从原有的微服务架构迁移到云原生架构时,直接使用服务网格方案,可以避免业务侧的大规模变更。</p> <h2>云原生带来的技术挑战</h2> <p>前文提到了目前云原生趋势下,一些技术层面的优势。但万物都有两面性,带来新鲜元素和机遇的同时,也会因为某些技术的加入带来一些挑战。</p> <h3>容器化与 K8s 引入的问题</h3> <p>在文章开头部分,我们提到了容器化技术采用共享内核的方式,共享内核带来轻量级的同时,也造成了隔离性上的不足。如果某一个容器,发生了容器逃逸,就可能出现主机被攻击的情况。因此为了应对这些安全挑战,也应运而生了安全容器等技术。</p> <p>此外容器镜像虽然提供了一个标准化的交付方式,但同样容器镜像也容易成为被攻击的类型,比如供应链攻击。</p> <p>同样在引入 K8s 后,也相应带来了组件安全方面的挑战。组件的增多导致了攻击面也增多,同时还额外带来了底层组件和依赖层面相关漏洞等。而在基础架构层面,从传统物理机或虚拟机迁移到 K8s 当中,会涉及到基础设施的改造成本以及更多的人力成本,去执行集群的数据备份、周期性升级以及证书续期等相关问题。</p> <p>同时在 Kubernetes 架构中,apiserver 作为整个集群的核心组件,需要处理来自内外部的所有流量。因此为了规避边界安全问题,如何保护 apiserver 也成为一个关键问题。比如我们可以使用 Apache APISIX 将其保护起来。</p> <h3>安全层面</h3> <p>使用新技术后,更需要在安全层面进行加倍关注:</p> <ul> <li>**在网络安全层面,**可以利用 Network Policy 这种方式进行流量的精细化控制。或者使用像 mTLS 这种方式进行连接加密,形成零信任网络。</li> <li>**在数据安全层面,**K8s 中提供了** **secret 资源进行机密数据的处理,但实际上它并不安全。因为 secret 资源当中的内容实际上只是进行了 Base64 编码,即你可以通过 Base64 解码去获取其中的具体内容,尤其是将这些内容放到 etcd 中后,如果你有 etcd 的访问权限也是可以直接读取的。</li> <li>**在权限安全层面,**还会涉及类似 RBAC 设置不合理的情况,进而导致攻击者可以利用相关 Token 去与 apiserver 进行通信,实现攻击目的。这种权限设置不合理的场景多见于 controller 和 operator。</li> </ul> <p><img src="https://static.apiseven.com/2022/09/26/633120ff0b888.png" alt="Authentication" referrerpolicy="no-referrer"></p> <h3>可观测性相关</h3> <p>云原生场景下,大多会涉及一些可观测性相关的操作,比如日志、监控等。</p> <p>在 K8s 中如果想要进行多样式的日志收集,就需要通过聚合的方式,在每一个 K8s 的 node 节点上直接进行采集。如果通过这种方式采集日志,就需要将应用程序全都输出到标准输出或者标准错误当中。</p> <p>但如果业务不进行相关改造,仍然选择将应用日志都写入到容器内的某一个文件中,那就意味着每一个实例当中都需要有一个 Sidecar 进行日志的采集,在部署架构时就会变得异常复杂。</p> <p>回到架构治理层面,在云原生环境下面,监控方案的选择也带来了一些挑战。因为方案选型一旦出现问题,那后续的使用成本是非常高的,一旦方向走错,带来的损失也是巨大的。</p> <p>同时在监控层面下,还会涉及一些容量问题。虽然在 K8s 当中部署应用程序时,可以简单通过配置它的限流限速等去限制该应用程序能够使用的资源细节。但是在 K8s 环境中,还是会比较容易出现资源超卖、过度占用资源的情况,以及因为这些情况产生的内存溢出等状况。</p> <p>此外,K8s 集群中还会存在另外一种情况,当整个集群或者 node 的资源不够了,它就会产生资源驱逐,将已经运行在某一个节点上的资源驱逐到其他节点上。假如说某个集群的资源比较紧张,一旦出现某个节点风暴,就会容易导致整个集群的业务全部挂掉。</p> <h3>应用变革及多集群模式</h3> <p>在应用架构变革层面,最核心的问题是服务发现。</p> <p>K8s 当中默认提供基于 DNS 的服务发现机制,但如果业务组成中包含云上业务和存量业务共存的状况,采用 DNS 服务发现机制进行处理时,就会比较复杂。</p> <p>同时企业选择云原生技术后,随着业务规模的扩大,就会逐渐去考虑多节点的处理方向,这时就会涉及到多集群问题。</p> <p>比如我们想通过多集群去给客户提供一个更高可用性的模式,这个时候就会涉及到服务在多集群之间的编排、多集群负载分配和同步配置,以及集群在多云及混合云场景下,如何处理和部署策略等问题。这些都是会面临的一些挑战。</p> <h2>APISIX 如何赋能数字化转型</h2> <p>Apache APISIX 是 Apache 软件基金会下的云原生 API 网关,它兼具动态、实时、高性能等特点,提供了负载均衡、动态上游、灰度发布(金丝雀发布)、服务熔断、身份认证、可观测性等丰富的流量管理功能。你可以使用 Apache APISIX 来处理传统的南北向流量,也可以处理服务间的东西向流量。</p> <p>目前在 Apache APISIX 中,基于上文描述的一些架构演进和应用变革,也衍生出了基于 APISIX 的 Ingress controller 和服务网格方案,助力企业更好地进行数字化转型。</p> <h3>APISIX Ingress 方案</h3> <p>Apache APISIX Ingress Controller 是一种 Kubernetes Ingress 控制器实现,它主要用作处理 Kubernetes 南北向流量的流量网关。</p> <p>APISIX Ingress Controller 架构类似 APISIX,是一种控制面和数据面分离的架构。其中,使用 APISIX 作为数据面,进行实际流量的处理。</p> <p>目前 APISIX Ingress Controller 支持以下三种配置方式,同时兼容 APISIX 的所有插件,开箱即用:</p> <ul> <li>支持 K8s 当中原生的 Ingress 资源。这种方式可以让 APISIX Ingress Controller 具有更高的适配性。目前来说,基本上是所有开源且具有影响力的 Ingress controller 产品中,APISIX Ingress Controller 是支持版本最多的一个产品。</li> <li>支持使用自定义资源。目前 APISIX Ingress Controller 的自定义资源是根据 APISIX 语义进行设计的一套 CRD 规范。使用自定义资源可以很方便地跟 APISIX 进行集成,更加原生。</li> <li>支持 Gateway API。作为下一代的 Ingress 标准,APISIX Ingress Controller 已开始支持 Gateway API(Beta 阶段)。随着 Gateway API 的发展,后续很有可能直接成为 K8s 的内置资源。</li> </ul> <p>相比 Ingress NGINX,APISIX Ingress Controller 存在以下优势:</p> <ul> <li><strong>架构分离</strong>。在 APISIX Ingress 中,采用了数据面和控制面分离的架构。当流量处理压力较大、想要进行扩容时,就可以简单操作进行数据面的扩容。这样就可以让更多的数据面对外提供服务,而不需要去对控制面进行任何调整。</li> <li><strong>扩展能力强,支持自定义插件</strong>。</li> <li><strong>数据面选用 APISIX,高性能全动态功能加持</strong>。得益于 APISIX 的全动态特性,在 APISIX Ingress 的使用中也可以尽可能的保护业务流量。</li> </ul> <p>目前 APISIX Ingress Controller 的使用用户网罗了国内外一些家喻户晓的企业。国内用户比如中国移动云开放平台(开放 API 和云 IDE 产品)、又拍云、地平线(车载 AI 系统)等,海外用户如欧洲的哥白尼参考系统(隶属欧盟的地球观测计划)。</p> <p>APISIX Ingress Controller 目前仍在持续迭代中,后续计划去完善更多功能:</p> <ol> <li>进行 Gateway API 的完整支持,去实现更多场景配置;</li> <li>支持 external service 服务代理;</li> <li>原生支持多注册中心,让 APISIX Ingress Controller 更加通用;</li> <li>进行架构更新,打造全新架构模型 ;</li> <li>与 Argo CD/Flux 等 GitOps 工具集成,打造丰富生态。</li> </ol> <p>如果你对 APISIX Ingress 方案感兴趣,也欢迎随时关注<a href="https://github.com/apache/apisix-ingress-controller">社区动态</a>,跟进产品的迭代与社区动向等。</p> <h3>APISIX 服务网格方案</h3> <p>目前,除了 API 网关和 Ingress 方案外,基于 APISIX 的服务网格方案也正在积极迭代中。</p> <p>基于 APISIX 的服务网格方案主要包含了两个部分,即控制面和数据面。控制面选择了 Istio,因为它行业领先且社区活跃,被多个厂商所支持。数据面则选择了 APISIX 去替换掉 Envoy,让 APISIX 的高性能和扩展性发挥作用。</p> <p>目前 APISIX 的服务网格网格仍在积极推进中,后续计划进行如下方向的迭代:</p> <ul> <li>进行 eBPF 加速,提高整个效能;</li> <li>进行插件能力集成,允许在服务网格体系中更好地使用 APISIX Ingress 能力;</li> <li>打造无缝迁移工具,为用户的迁移过程提供更加便捷的工具,简化流程。</li> </ul> <p>因此总体来看,在云原生时代下,架构和技术的演进为我们带来了机会和挑战。Apache APISIX 作为云原生网关,也一直致力于为云原生趋势进行更多的技术适配与集成。基于 APISIX 的各类解决方案,也开始帮助企业用户进行数字化转型,助力企业更平稳地过渡到云原生赛道。</p>

什么是 API 网关,为什么它在云原生时代如此重要?

<p>API(Application Programming Interface) 是各个不同的应用程序和系统之间互相调用和传输数据的标准方式。在很多的开发团队中都是使用 API-first 的模式,围绕着 API 来进行产品的迭代,包括测试、Mock、文档、API 网关、Dev Portal 等,这就是 API 全生命周期管理(Full Life Cycle API Management)。</p> <h2>API 存在的问题</h2> <p>在 API 出现之前,数据的传输和交换并没有标准的方式,大多数情况下是通过数据库、Excel 表格、文本,或者是 FTP,不同的系统和程序通过各种五花八门的方式来沟通。这些混乱的背后,隐藏了巨大的开发成本和安全隐患:权限控制、数据精细管控、限流限速、审计等,都只能用笨重的方式来解决。这就是计算机世界中的“巴别塔”(Tower of Babel),因此只有解决了不同语言开发的系统以及不同存储方案带来的问题,才有机会构建足够复杂的产品。</p> <p>而 API 的出现,则成功地解决了巴别塔问题,开发者只需要关心其他系统对外暴露的 API 即可,无需关心底层实现和细节。</p> <p>我们熟知的手机 App、网络游戏、视频直播、远程会议和 IoT 设备,都离不开终端设备与服务端的连接和数据传输,这些都是通过 API 完成的。</p> <h2>为什么需要 API 网关</h2> <p>API 网关是 API 全生命周期管理中的关键基础组件,负责生产环境中 API 的配置、发布、版本回滚、安全、负载均衡等。API 网关是所有终端流量的入口,负责把终端的 API 请求路由到正确的上游服务进行处理,然后再把返回的数据返回给原始请求方,同时保证整个过程的安全、可靠和低延迟。</p> <p><img src="https://static.apiseven.com/2022/09/06/63171bed80a41.png" alt="1.png" referrerpolicy="no-referrer"></p> <p>在最开始 API 数量不多的情况下,API 网关往往是一个由 web server 和上游服务两部分拼接而成的虚拟组件:由 Apache 和 NGINX 等组件完成最简单的路由转发、反向代理和和负载均衡,其他功能比如身份认证、限流限速等,则依靠上游服务自身进行实现。</p> <p>但是随着 API 的数量越来越多,喜欢“偷懒”的开发者们发现了一个严重的问题:他们需要在不同的上游服务中,重复实现身份认证、限流限速、日志等通用的功能,这不仅会浪费开发资源,而且一旦需要修改这部分的代码,就需要修改很多代码,这是版本管理和升级维护的噩梦。怎么办呢?聪明的开发者们很快就找到了解决方案:把这些通用的功能抽象到一个统一的组件中,把上述的两层结构改为一层结构,从上游的逻辑代码中剥离与业务无关的功能,然后增强 Apache 和 NGINX 这类组件。这就是第一代 API 网关的诞生过程。</p> <p>让 API 网关承载更多非业务逻辑的功能,这就是 API 网关一直以来的进化方向。在这个过程中,前端开发者和后端开发者们,为了让产品能够更快的迭代,就对 API 网关提出了越来越多的需求,并不仅仅局限于负载均衡、反向代理、身份认证等传统功能,还在 gRPC、GraphQL、可观测性等方面提出了很多新的功能。</p> <h2>API 网关的作用</h2> <p>为了让 API 网关更灵活高效,开发者们在底层上也做了非常多创新,比如:</p> <ul> <li>功能插件化。随着 API 网关上承载的功能越来越多,如何让这些功能之间更好的隔离以及让二次开发变得更加简单?插件,是完美的解决方案。因此,现在主流的 API 网关均采用插件来实现各种功能,比如在 Apache APISIX 里面叫做 Plugins,在 Envoy 中则称之为 Filters,它们的含义是相同的。插件化,可以让网关的开发者不再关心底层实现,用较少的开发资源就可以实现一个新的功能。</li> <li>数据面和控制面的分离。在第一代 API 网关的实现中,数据面和控制面是在同一个进程中实现的,这样足够简单,但也带来了很大的安全隐患。由于数据面是直接对外提供服务的,如果被黑客入侵,就有机会获取到控制面的数据(比如 SSL 证书)和控制权,造成更大的破坏。因此,现在大部分的开源 API 网关,都是将两者分别部署的,中间通过关系型数据库或者 etcd 来进行配置的管理和同步。</li> </ul> <p>以 Apache APISIX 为例,下面的架构中诠释了以上两个创新:</p> <p><img src="https://static.apiseven.com/2022/09/06/631715b491146.png" alt="2.png" referrerpolicy="no-referrer"></p> <h2>云原生时代下的挑战</h2> <p>过去十年,IT 领域最大的技术变革就是云原生。诞生于 2013 年的 Docker 拉开了云原生的帷幕,从此裸金属、虚拟机开始被容器所替代,单体架构开始被微服务所替代。但是云原生并不是简单的技术革命,其背后的推动力主要来自互联网产品的快速发展和激烈竞争,云原生相关的技术生逢其时,迅速流行并替代了之前的很多技术组件和方案。</p> <p>具体到 API 网关在云原生中的挑战,主要来自以下两个方面:</p> <h3>单机架构到微服务的转型</h3> <p>在微服务架构逐渐被开发者认可和落地后,该架构释放了巨大的技术红利:每个微服务可以按照自己的节奏进行升级和发布,不需要担心与其他服务的耦合。产品的迭代因此变得敏捷,每天都可以进行几十次甚至几百次的发布。</p> <p>但与此同时,微服务的发展也带来了一些副作用,比如:</p> <ul> <li>API 和微服务的数量从最初的几十个,增长到几千个,甚至几万个;</li> <li>出现故障时如何快速定位是哪一个 API 引起的?</li> <li>如何保证 API 的安全?</li> <li>如何做到服务熔断和服务降级?</li> </ul> <p>API 网关无法独立解决安全性、可观察性、灰度发布等问题。 它需要与 Prometheus、Zipkin、Skywalking、Datadog、Okta 等众多开源项目和 SaaS 服务合作,为企业提供更好的解决方案。</p> <h3>动态和集群化管理</h3> <p>容器和 Kubernetes 的普及,让动态成为所有云原生基础组件的标准特性。在 Kubernetes 的环境中,容器在不断的生成和销毁,弹性伸缩成为一个必选项而不是可选项。</p> <p>想象一个场景:一家互联网电商公司做了一次促销,大量的用户在一个小时内涌入,促销结束后就会离开。对于传统架构的公司来说,他们需要事先采购一批物理服务器,来应对这些高峰时候的 API 流量;但是对于云原生架构的公司来说,就可以随时使用公有云上的弹性资源,根据 API 请求的数量,自动的调整网络、计算、数据库等资源的规模即可。那么伴随着容器弹性伸缩而来的技术挑战是:</p> <ul> <li>上游服务不断更换 IP 地址和端口;</li> <li>IP 黑白名单的频繁更新;</li> <li>服务健康的及时检测和异常处理;</li> <li>API 的频繁发布;</li> <li>服务注册和发现的及时性;</li> <li>SSL 证书的热更新和自动轮转;</li> </ul> <p>想要解决上述这些挑战,均需要依赖于动态。以 NGINX 为代表的第一代 API 网关,动态能力是非常弱的。因为 NGINX 是本地静态配置文件驱动的,所以变更任何配置都需要 reload NGINX 服务才能生效,这在云原生时代是不能被企业接受的。这就是第一代 API 网关的技术痛点之一。</p> <p>在中国,有一家类似微软 Office 365 的 SaaS 办公软件公司 -- WPS,他们有数百台物理机在运行 Apache APISIX ,有近万核 CPU 在处理来自客户端的 API 请求,每天处理数百亿次 API 请求。</p> <p>在这个超大规模的 API 网关环境下,开发者不可能去逐个修改每一个 API 网关的配置然后 Reload,他们希望有一个统一的控制台来操作整个集群。可惜的是,第一代 API 网关诞生的年代,并没有这么大的实例规模,也就没有考虑集群管理的需求。</p> <h2>下一代 API 网关</h2> <p>上述挑战和痛点,逐渐催生了新一代的 API 网关。</p> <p>和第一代 API 网关不同的是,云原生时代诞生的下一代 API 网关是在开源社区的驱动下快速成长的。借助社区和众多开源贡献者的力量,这些 API 网关有机会形成一个正向的迭代和进化:</p> <ul> <li>更快速的收集一线开发者及用户的需求和痛点</li> <li>在开源项目中尝试解决这些问题</li> <li>开源项目变得更加好用,吸引更多开发者使用</li> </ul> <p>于是,我们看到下一代 API 网关突破了传统网关的负载均衡和反向代理的定位,而是承担起了 API 和流量的连接、调度、过滤、分析、协议转换、治理、集成等更多的职责。</p> <p><img src="https://static.apiseven.com/2022/09/06/631716a27bf67.png" alt="3.png" referrerpolicy="no-referrer"></p> <h3>支持更低成本的二次开发</h3> <p>同时,让开发者能够以更低的成本进行二次开发,也成为了下一代 API 网关的亮点。集成是 API 网关的重要功能之一,对于下游是协议解析和各种协议的转换,包括 GraphQL、gRPC、Dubbo 等;对于上游是集成 Okta、Keycloak、Datadog、Prometheus 等身份认证、可观测性服务,以及公司内部的认证、日志、审计等服务。</p> <p>API 网关不可能覆盖集成过程中所有的组件,这时候不可避免的需要开发者通过插件的方式进行二次开发,来满足自己的业务需求。</p> <p>不同的 API 网关提供了不同的二次开发的编程语言和开发方式,Apache APISIX 和 Kong 都可以使用 Lua 来编写原生插件,Envoy 是使用 C++ 编写原生插件。同时,Apache APISIX 还可以使用 Go、Python、Node、Java 和 Wasm 来编写插件,这些主流的开发语言已经可以覆盖绝大部分开发者了。</p> <table> <thead> <tr> <th style="text-align:center"></th> <th style="text-align:center"></th> <th style="text-align:center"></th> <th style="text-align:center"></th> </tr> </thead> <tbody> <tr> <td style="text-align:center"><strong>API Gateway</strong></td> <td style="text-align:center"><strong>APISIX</strong></td> <td style="text-align:center"><strong>Kong</strong></td> <td style="text-align:center"><strong>Envoy</strong></td> </tr> <tr> <td style="text-align:center">languages for custom plugins</td> <td style="text-align:center">Lua<br>Go<br>Java<br>Python<br>Node<br>Wasm</td> <td style="text-align:center">Lua<br>Go<br>Wasm(not open source)</td> <td style="text-align:center">C++<br>Lua<br>Wasm</td> </tr> <tr> <td style="text-align:center">Development difficulty</td> <td style="text-align:center">Low</td> <td style="text-align:center">Medium</td> <td style="text-align:center">High</td> </tr> </tbody> </table> <p>开发者不必去学习 Lua 和 C++,就可以使用自己熟悉的编程语言在下一代 API 网关上进行开发,这让基础组件的开发变得更加简单。</p> <p>开源和易于二次开发,是下一代的 API 网关最重要的特点,它把更多的选择权留给了开发者自身,同时,开发者也可以更加放心的在多云、混合云的环境下使用 API 网关,不用担心被云供应商锁定。</p> <h2>基于双十一的 API 网关流量处理场景</h2> <p>让我们用一个具体的例子来解释下 API 网关的作用。</p> <p>在双十一时,电商都会有各种各样的商品促销活动,这段时间的 API 请求量是平时的几十倍。让我们先来看下如果没有 API 网关将会是怎样的技术架构:</p> <p><img src="https://static.apiseven.com/2022/09/06/63171dbd62317.png" alt="4.png" referrerpolicy="no-referrer"></p> <p>可以看到,在 Order 和 Payment 服务中,身份认证和日志记录功能是重复的。一个电商的服务,一般会有数千个不同的服务组成,这时候就会有大量的代码和功能是重复开发的。</p> <p>下面是增加了 API 网关之后的架构图:</p> <p><img src="https://static.apiseven.com/2022/09/06/63171e014eb3d.png" alt="5.png" referrerpolicy="no-referrer"></p> <p>从上图可以看到,我们在 API 网关层统一了公共的服务,后端服务只需要关心自身业务,为弹性伸缩提供了更多可能。</p> <p>当促销开始时,客户端大量 API 请求涌入 API 网关的时候,后端服务需要进行快速的弹性伸缩,为了保障关键业务不受突发流量的影响,我们需要在 API 网关上识别恶意爬虫并实现限流限速、服务降级和熔断。此时,我们可以暂时关闭部分服务,比如商品评价、快递查询等。但是库存信息、购买功能、支付功能等核心业务是绝对不能出现故障的,因此我们需要通过 k8s 来管理容器服务,再生成更多的服务副本来保证它的正常运行。此时 API 网关需要将客户端的 API 请求路由到新生成的副本服务,并且自动移除出现故障的服务,如下图所示。</p> <p><img src="https://static.apiseven.com/2022/09/06/63171e2671970.png" alt="6.png" referrerpolicy="no-referrer"></p> <h2>总结</h2> <p>API 网关并不是一个新的基础中间件,而是在产品快速迭代和技术架构的变迁中变得越来越重要。而下一代云原生 API 网关的出现则解决了企业用户在集群管理,动态,生态,可观测性以及安全性等方面的痛点。</p> <p>API 网关不仅可以处理 API 的流量,也可以来处理 Kubernetes Ingress 和服务网格的流量,进一步的降低开发者的学习成本,帮助企业更好的统一管理流量。</p> <p>想了解更多关于 Apache APISIX、API7 企业产品和 API7 Cloud 产品,可以<a href="https://apiseven.mikecrm.com/pvdVjd5">联系我们</a>。</p>

Cloud 产品中有关 TCP RST 的问题排查过程

<p>最近在项目中遇到了一个因为 TCP RST 而导致集成测试用例运行失败的问题,简单用文字在此跟大家分享一下。</p> <h2>背景信息</h2> <p>我们的后端项目是包含了多个组件的微服务系统,数据库使用的是 Postgres。通常在进行集成测试时,我们会使用 <a href="https://github.com/kubernetes-sigs/kind">Kind</a> 将整个后端组件以及依赖的 Postgres、Redis 等中间件运行起来,然后通过 port-forward 方式暴露到宿主机上,这样运行在宿主机上的 <a href="https://github.com/onsi/ginkgo">Ginkgo</a> (Golang 集成测试框架)就可以访问到 Kind 中运行的后端服务了。这一套测试框架在依赖 Kubernetes 的项目中比较常见,使用起来也很便捷。</p> <p>而这次的问题出现在,我们将集成测试中的 k8s.io/client-go 依赖从 v0.22.4 升级到 v0.24.0 版本后,测试用例中通过 port-forward 方式访问 Postgres 时,总是会出现失败的情况。</p> <h2>排查过程一:分析并找到初步原因</h2> <h3>锁定 Port-Forward 断开的原因</h3> <p>刚发现问题的时候,我们并不知道是因为升级 k8s.io/client-go 导致的问题,因为出现问题的 Pull Request 中还有很多其他的变更。所以我们首先还是通过查看错误日志,得到了如下信息:</p> <pre><code>E0610 07:20:46.199054 25339 portforward.go:406] an error occurred forwarding 35169 -&gt; 5432: error forwarding port 5432 to pod d9229db6040a461f038cf546c5efc39c407f48d015b603855882f2a94a9c028a, uid : failed to execute portforward in network namespace "/var/run/netns/cni-d75c685d-0da5-a6c9-9891-ffba16e23531": read tcp4 127.0.0.1:51062-&gt;127.0.0.1:5432: read: connection reset by peer E0610 07:20:46.199530 25339 portforward.go:234] lost connection to pod </code></pre> <p>对比了旧代码的运行日志,发现其中也会打印第一行日志,但是没有第二行 <code>lost connection to pod</code>。 根据这个关键信息,我们继续翻看了 kubernetes portforward 包下的代码:</p> <pre><code class="language-go">// wait for interrupt or conn closure select { case &lt;-pf.stopChan: case &lt;-pf.streamConn.CloseChan(): runtime.HandleError(errors.New("lost connection to pod")) } </code></pre> <p>上述代码显示,只有在 <code>pf.streamConn</code> 被关闭时才会打印 <code>lost connection to pod</code>,搜索 <code>pf.streamConn.Close()</code> 相关代码,最终锁定了以下信息:</p> <pre><code class="language-go">// always expect something on errorChan (it may be nil) err = &lt;-errorChan if err != nil { runtime.HandleError(err) pf.streamConn.Close() } </code></pre> <p>Git Blame 这段代码,发现是由于该 <a href="https://github.com/kubernetes/kubernetes/pull/103526">PR</a> 实现的一个优化导致了问题。</p> <blockquote> <p>该 PR 涉及的优化细节为:在 port-forward 的 Pod 侧连接(kubelet -&gt; postgres)出现 error 时,让 port-forward 自身被关闭。这是为了解决 kubectl 对某个 pod 执行了 port-forward 后,该 pod 死掉的情况下(比如被删除了),kubectl 可以自动退出,而不至于一直继续运行。</p> </blockquote> <p>经过上面的分析,我们锁定了是因为 port-forward 在 pod 侧的连接出现了 error,结合日志 <code>read tcp4 127.0.0.1:51062-&gt;127.0.0.1:5432: read: connection reset by peer</code> 分析,也就是说在 kubelet 发起的 51062 端口到 Postgres 5432 的 TCP 连接中,出现了 Postgres 发出的 TCP RST 包。</p> <h3>为什么 Postgres 会发出 TCP RST 包?</h3> <p>下一步就是要分析为什么 Postgres 会发出 RST 包。在这里,我们可以使用最新版本(v1.24.2)的 kubectl 来尝试复现集成测试中遇到的问题。</p> <p>首先开启 port-forward。</p> <pre><code class="language-go">❯ ./kubectl port-forward pod/postgres-0 5432:5432 Forwarding from 127.0.0.1:5432 -&gt; 5432 Forwarding from [::1]:5432 -&gt; 5432 </code></pre> <p>然后使用 psql 通过 port-forward 查询 Postgres。需注意,由于我们的 Postgres 开启了客户端证书校验,所以需要为 psql 指定客户端证书和私钥。</p> <pre><code class="language-go">PGPASSWORD="123456" PGSSLCERT=./tls.crt PGSSLKEY=./tls.key psql -t -h 127.0.0.1 -U postgres -d postgres -c "select 1;" </code></pre> <p>port-forward 日志打印如下:</p> <pre><code class="language-go">Handling connection for 5432 E0711 00:01:28.334216 3041058 portforward.go:406] an error occurred forwarding 5432 -&gt; 5432: error forwarding port 5432 to pod ebb9210ccf74b8047be4c6e8a23237c460a33235b2f6c65430ecd680f70e71cf, uid : failed to execute portforward in network namespace "/var/run/netns/cni-d8bfc8cf-2434-540c-9e60-f71f81de7ac0": read tcp4 127.0.0.1:36252-&gt;127.0.0.1:5432: read: connection reset by peer E0711 00:01:28.334458 3041058 portforward.go:234] lost connection to pod </code></pre> <p>在复现的同时,我们使用 tcpdump 抓取 <code>postgres 5432</code> 端口的包以及使用 strace 记录 Postgres 进程发起的系统调用。根据复现异常时的日志中 <code>36252</code> 端口作为关键字,在 tcpdump 和 strace 输出中过滤得到如下信息。</p> <p>tcpdump 输出信息:</p> <pre><code class="language-text">00:01:28.330748 IP 127.0.0.1.36252 &gt; 127.0.0.1.5432: Flags [P.], seq 1866:1903, ack 2595, win 512, options [nop,nop,TS val 2518407611 ecr 2518407610], length 37 00:01:28.330750 IP 127.0.0.1.5432 &gt; 127.0.0.1.36252: Flags [.], ack 1903, win 512, options [nop,nop,TS val 2518407611 ecr 2518407611], length 0 00:01:28.331087 IP 127.0.0.1.5432 &gt; 127.0.0.1.36252: Flags [P.], seq 2595:2683, ack 1903, win 512, options [nop,nop,TS val 2518407611 ecr 2518407611], length 88 00:01:28.331789 IP 127.0.0.1.36252 &gt; 127.0.0.1.5432: Flags [P.], seq 1903:1930, ack 2683, win 512, options [nop,nop,TS val 2518407612 ecr 2518407611], length 27 00:01:28.331793 IP 127.0.0.1.5432 &gt; 127.0.0.1.36252: Flags [.], ack 1930, win 512, options [nop,nop,TS val 2518407612 ecr 2518407612], length 0 00:01:28.331803 IP 127.0.0.1.36252 &gt; 127.0.0.1.5432: Flags [P.], seq 1930:1954, ack 2683, win 512, options [nop,nop,TS val 2518407612 ecr 2518407612], length 24 00:01:28.331805 IP 127.0.0.1.5432 &gt; 127.0.0.1.36252: Flags [.], ack 1954, win 512, options [nop,nop,TS val 2518407612 ecr 2518407612], length 0 00:01:28.332296 IP 127.0.0.1.5432 &gt; 127.0.0.1.36252: Flags [P.], seq 2683:2707, ack 1954, win 512, options [nop,nop,TS val 2518407613 ecr 2518407612], length 24 00:01:28.333718 IP 127.0.0.1.5432 &gt; 127.0.0.1.36252: Flags [R.], seq 2707, ack 1954, win 512, options [nop,nop,TS val 2518407614 ecr 2518407612], length 0 </code></pre> <p>strace 输出信息:</p> <pre><code class="language-text">3431299 accept(3, {sa_family=AF_INET, sin_port=htons(36252), sin_addr=inet_addr("127.0.0.1")}, [128-&gt;16]) = 9 3431299 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fbba752ec50) = 1323730 3431299 close(9) ... 1323730 fcntl(9, F_GETFL) = 0x2 (flags O_RDWR) 1323730 fcntl(9, F_SETFL, O_RDWR|O_NONBLOCK) = 0 ... 1323730 recvfrom(9, "\27\3\3\0 ", 5, 0, NULL, NULL) = 5 1323730 recvfrom(9, "...", 32, 0, NULL, NULL) = 32 1323730 sendto(9, "...", 88, 0, NULL, 0) = 88 1323730 recvfrom(9, "\27\3\3\0\26", 5, 0, NULL, NULL) = 5 1323730 recvfrom(9, "...", 22, 0, NULL, NULL) = 22 1323730 sendto(9, "...", 24, 0, NULL, 0) = 24 1323730 exit_group(1) = ? 1323730 +++ exited with 1 +++ </code></pre> <p>通过分析上述 strace 结果,我们会发现 Postgres 采用了很传统的进程模型,即每个连接启动一个子进程去处理。在上述日志中,3431299 进程在接收到新的连接(36252 -&gt; 5432)时克隆出了 1323730 子进程去处理。</p> <p>通过对比 tcpdump 和 strace 的输出,会发现来自 36252 最后一个 24 字节大小的包没有被 1323730 进程读取,而这正是导致内核返回 RST 包的原因。</p> <p>在正常情况下,如果进程退出或者对 Socket 主动执行 Close 系统调用,内核会发出 FIN 包,进入四次挥手流程。但是在当前的场景下,内核发现在断开连接时还存在一些数据没有被应用层读取,应用就已经退出的情况,导致内核不得不使用 RST 包,以此来告诉客户端“你发送的一些数据没有被服务端读取,这是一次异常的 TCP 断连”。</p> <p>那么这个没有被读取的 24 字节包到底是什么呢?</p> <p><img src="https://static.apiseven.com/2022/10/20/6350ec7dac8db.png" alt="24 字节包" referrerpolicy="no-referrer"></p> <p>前文中提到我们使用的 Postgres 是开启了 mTLS 模式的,所以整个连接都是 TLS。在断开连接时,就会发出 TLS 协议中的 “Close Notify” 包。下图展示了一个 TLS 连接的生命周期:</p> <p><img src="https://static.apiseven.com/2022/10/20/6350ec7cc7386.png" alt="TLS 连接生命周期图" referrerpolicy="no-referrer"></p> <p>结合到 Postgres TLS 连接场景下,Client (psql)和 Server(Postgres)都会给对方发送 <code>close_notify</code>,但是也都没有等对方的响应就直接关闭了 TCP 连接,所以两边都各自产生了一个 RST,下面是 psql 侧抓包的结果:</p> <pre><code class="language-text">00:36:56.015219 IP 127.0.0.1.39248 &gt; 127.0.0.1.5432: Flags [P.], seq 1903:1930, ack 2683, win 512, options [nop,nop,TS val 2520535295 ecr 2520535295], length 27 00:36:56.015229 IP 127.0.0.1.39248 &gt; 127.0.0.1.5432: Flags [P.], seq 1930:1954, ack 2683, win 512, options [nop,nop,TS val 2520535295 ecr 2520535295], length 24 00:36:56.015426 IP 127.0.0.1.39248 &gt; 127.0.0.1.5432: Flags [F.], seq 1954, ack 2683, win 512, options [nop,nop,TS val 2520535296 ecr 2520535295], length 0 00:36:56.015427 IP 127.0.0.1.5432 &gt; 127.0.0.1.39248: Flags [.], ack 1954, win 512, options [nop,nop,TS val 2520535296 ecr 2520535295], length 0 00:36:56.016076 IP 127.0.0.1.5432 &gt; 127.0.0.1.39248: Flags [P.], seq 2683:2707, ack 1955, win 512, options [nop,nop,TS val 2520535296 ecr 2520535296], length 24 00:36:56.016104 IP 127.0.0.1.39248 &gt; 127.0.0.1.5432: Flags [R], seq 2542047690, win 0, length 0 </code></pre> <p>前三个包分别是 Postgres 协议中的 Termination、SSL 层的 close_notify 和 TCP 层 FIN。psql 一次性就把自己断开连接要发送的指令都发完了,结果服务端又返回了一个 SSL close_notify。</p> <p>此时因为这个 Socket 的 fd 已经被 psql 关掉了,内核也就知道这个数据包 psql 是没法读取的了,也就不得不出现一个 RST 了。</p> <h3>总结</h3> <p>经过上面的分析可以看出,在 Postgres 开启 TLS 模式的前提下,客户端跟 Postgres 的连接断开流程中一定会产生 RST,所以我们暂时调高了集成测试中 Postgres 客户端的 IdleTimeout,让连接在测试运行过程中不要断开,一直保持连接状态,暂时绕过了这个问题。</p> <h2>排查过程二:解决新问题</h2> <p>Postgres 的问题暂时解决后,很快又发现了新的问题。一些关于 APISIX HTTPS 的测试用例会出现偶发挂掉。</p> <p>通过查看日志,我们发现对于 APISIX HTTPS 端口的 port-forward 挂掉状况,总是发生在请求了 60s 后,跟默认的 <code>keepalive_timeout</code> 配置符合。通过抓包,我们确定了闲置的连接确实在 60s 后被 APISIX 主动断开了。以下是断开连接时的抓包截图:</p> <p><img src="https://static.apiseven.com/2022/10/20/6350ec7d40659.png" alt="抓包截图 1" referrerpolicy="no-referrer"></p> <p>通过图中信息可以看到,这次断开连接的流程确实存在 RST 包,原因与 Postgres 场景下类似。即 APISIX 断开连接时会发出 ssl close notify 包,客户端收到后会回复 <code>ssl close notify</code>,但是 APISIX 并没有读取客户端发送的这条返回信息。</p> <p>但问题是发生在上述断开连接的流程中, port-forward 并没有因为这个 RST 包被关闭掉。原因是 APISIX 已经发出了 FIN 包,kubelet 在收到 FIN 包后(read EOF),也开始关闭自己的 fd,也就不会再读取这个 fd 了,所以这个 RST 对它没有影响。</p> <p>但同时我们注意到,APISIX 发送 SSL Close Notify(SSL_shutdown) 和发出 FIN (close)其实是两次顺序执行的调用,那么有没有可能是在这两步之间,客户端的 SSL Close Notify 横插一脚,也就是说 FIN 包还没有发出去,客户端的 SSL Close Notify 就已经到了(因为整个网络都是在同一台宿主机上,延迟很低),此时就可能会导致 RST。</p> <p>思来想去我们认为只有上面这种可能,便开始尝试复现这种情况。首先将 APISIX 的 <code>keepalive timeout</code> 配置调小(3s),这样可以快速模拟 timeout 后断开连接,然后执行一个客户端每次发起 10 个连接,等待 APISIX 关闭连接。循环执行,直到 port-forward 挂掉,在一段时间后,果然复现了问题,以下是异常请况下的抓包截图:</p> <p><img src="https://static.apiseven.com/2022/10/20/6350ec7d1adcc.png" alt="抓包截图 2" referrerpolicy="no-referrer"></p> <p>抓包结果显示,上述猜想是正确的。</p> <h3>总结</h3> <p>在网络延迟很低的情况下,由 APISIX 主动断开的 SSL 连接可能会触发 RST,所以我们在集成测试中将客户端的 <code>keepalive timeout</code> 配置为小于 APISIX 的 60s,这样就可以让客户端先断开连接,也就不会触发 APISIX 可能发出 RST 的情况。</p> <h2>后记</h2> <p>可以看到 APISIX 和 Postgres 在断开 SSL 连接时,都不会理会客户端的 Close Notify,TLS 的标准中可能压根也没有规定一定要读取对端的 Close Notify 包,而这正是导致出现 TCP RST 的根本原因。</p> <p>以上就是本次分析 RST 问题的全部过程,其中关于 Postgres、Kubelet 、APISIX、内核等行为的分析大多都是基于使用 tcpdump 和 strace 观测所得,尚没有深入到源码,所以在理解上可能有误,欢迎大家指摘。但整个分析思路和方法对类似的 RST 问题是通用的,可以进行相关参考。</p>

有了 NGINX 和 Kong,为什么还需要 Apache APISIX

<p>云原生时代,动态和可观测性成为衡量 API 网关的标准之一。Apache APISIX 自诞生之初就一直跟随着云原生的脚步前行。然而 Apache APISIX 作为一个诞生刚刚三年的新一代网关,为什么可以从诞生 20 多年的 NGINX 与开源 8 年的 Kong 中突出重围,成为云原生时代最流行以及最活跃的网关?我认为其中最重要的原因是解决了开发者和企业在使用 NGINX 和 Kong 中的痛点。</p> <h2>NGINX 与 Kong 的痛点</h2> <p>在单体服务时代,使用 NGINX 可以应对大多数的场景,而到了云原生时代,NGINX 因为其自身架构的原因则会出现两个问题:</p> <ul> <li>首先是 NGINX 不支持集群管理。几乎每家互联网厂商都有自己的 NGINX 配置管理系统,系统虽然大同小异但是一直没有统一的方案。</li> <li>其次是 NGINX 不支持配置的热加载。很多公司一旦修改了配置,重新加载 NGINX 的时间可能需要半个小时以上。并且在 Kubernetes 体系下,上游会经常发生变化,如果使用 NGINX 来处理就需要频繁重启服务,这对于企业是不可接受的。</li> </ul> <p>而 Kong 的出现则解决了 NGINX 的痛点,但是又带来了新的问题:</p> <ul> <li>Kong 需要依赖于 PostgreSQL 或 Cassandra 数据库,这使 Kong 的整个架构非常臃肿,并且会给企业带来高可用的问题。如果数据库故障了,那么整个 API 网关都会出现故障。</li> <li>Kong 的路由使用的是遍历查找,当网关内有超过上千个路由时,它的性能就会出现比较急剧的下降。</li> </ul> <p>而 APISIX 的出现则解决了上述所有问题,成为了云原生时代最完美的 API 网关。那么 Apache APISIX 的优势到底是什么?为什么可以在短短三年的时间里成为全世界最活跃的网关?</p> <h2>APISIX 的优势</h2> <h3>优异的架构</h3> <p>首先 Apache APISIX 拥有优异的架构,现在很多应用都在向微服务、容器化迁移,形成新的云原生时代。云原生作为当下的技术潮流,将重写传统企业的技术架构。而 APISIX 自诞生之初就跟随技术潮流,并将其设计为云原生架构:</p> <p><img src="https://static.apiseven.com/2022/blog/0729/1.png" alt="APISIX" referrerpolicy="no-referrer"></p> <p>如上图所示,左右分别是 APISIX 的数据面(Data Plane)和控制面(Control Plane):</p> <ul> <li>数据面:以 NGINX 的网络库为基础(未使用 NGINX 的路由匹配、静态配置和 C 模块),使用Lua 和 NGINX 动态控制请求流量;</li> <li>控制面:使用 etcd 来存储和同步网关的配置数据,管理员通过 Admin API 或者 Dashboard 可以在毫秒级别内通知到所有数据面节点。</li> </ul> <p>在更新数据上,Kong 采用轮询数据库的方式,但是可能需要 5-10 秒才能获取到最新的配置;而 APISIX 则采用监听 etcd 的配置变更的方式,可以将时间控制在毫秒级,达到实时生效的效果。 而且由于 APISIX 和 etcd 均支持多点部署,因此在 APISIX 当前架构中,任何一个服务出现异常宕机等事故,都不会影响 APISIX 正常对外提供服务的能力。</p> <h3>完善的生态</h3> <p>下图为 APISIX 的生态图,从该图可以准确看到 APISIX 已经支持的 7 层协议有 HTTP(S)、HTTP2、Dubbo 和物联网协议 MQTT 等等,4 层协议有 TCP/UDP。 右侧部分则是一些开源或者 SaaS 服务,比如 SkyWalking、Prometheus 、Vault 等等。而最下面则是比较常见的操作系统环境、云厂商和硬件环境。而作为一个开源软件,APISIX 也支持在 ARM64 的服务器上运行。</p> <p><img src="https://static.apiseven.com/2022/blog/0729/2.PNG" alt="APISIX's Ecosystem" referrerpolicy="no-referrer"></p> <p>APISIX 不仅支持众多协议与操作系统,而且也支持多语言编程插件。APISIX 诞生之初仅支持使用 Lua 语言编写插件,这种情况下就需要开发者掌握 Lua 和 NGINX 相关的技术栈。然而 Lua 和 NGINX 属于相对小众的技术,开发者很少。因此我们在 APISIX 上支持了多语言开发插件,目前已经正式支持了 Java、Golang、Node.js、Python 等语言。</p> <p><img src="https://static.apiseven.com/2022/blog/0729/3.png" alt="programming language" referrerpolicy="no-referrer"></p> <h3>活跃的社区</h3> <p>下图是贡献者增长曲线,其中横坐标代表时间线,纵坐标代表贡献者总数。我们可以看到 APISIX 和 Kong 这两个项目相对更活跃,APISIX 的增长速度从开源第一天就保持着非常不错的增长率,在以接近 Kong 两倍的速度快速成长,并且贡献者数量已经远远超过了 Kong,由此可见 APISIX 受欢迎程度。当然评价一个项目活跃度还有很多其他方法,比如查看每月活跃 Issue、PR 总数等方式,值得高兴的是 APISIX 在这些方面也是一骑绝尘。</p> <p><img src="https://static.apiseven.com/2022/blog/0729/4.png" alt="Contributor graph" referrerpolicy="no-referrer"></p> <h2>APISIX 的应用场景</h2> <p>从下图中,相信你已经看出 APISIX 的目标:<strong>统一代理基础设施</strong>。</p> <p><img src="https://static.apiseven.com/2022/blog/0729/5.png" alt="APISIX Application scenarios" referrerpolicy="no-referrer"></p> <p>也许你会有疑问:APISIX 要支持这么多场景,是否会让 APISIX 变得四不像?</p> <p>因为 APISIX 的核心是高性能代理服务,自身不绑定任何环境属性。当它演变为 Ingress、服务网格等产品时,都是外部服务与 APISIX 配合,变化的是外部程序而不是 APISIX 自身,下面将逐步为大家介绍 APISIX 是如何支持这些场景的。</p> <h3>Load Balancer 和 API 网关</h3> <p>首先是针对传统的 LB 和 API 网关场景,因为 APISIX 基于 NGINX + LuaJIT 实现,所以天然具备高性能、安全等特性,并且原生支持了动态 SSL 证书卸载、SSL 握手优化等功能,在负载均衡的服务能力上也更优秀。从 NGINX 切换到 APISIX 不仅性能不会下降,而且可以享受到动态、统一管理等特性带来的管理效率的提升。</p> <h3>微服务网关</h3> <p>APISIX 目前支持多种语言编写扩展插件,可以解决东西向微服务 API 网关面临的主要问题——异构多语言和通用问题。内置支持的服务注册中心有 Nacos、etcd、Eureka 等,还有标准的 DNS 方式,可以平滑替代 Zuul、Spring Cloud Gateway、Dubbo 等微服务 API 网关。</p> <h3>Kubernetes Ingress</h3> <p>目前 K8s 官方 Kubernetes Ingress Controller 项目主要基于 NGINX 配置文件的方式,所以在路由能力和加载模式上稍显不足,并且存在一些明显劣势。比如添加、修改任何 API 时,需要重启服务才能完成新 NGINX 配置的更新,但重启服务,对线上流量的影响是非常大的。</p> <p>而 <a href="https://apisix.apache.org/zh/docs/ingress-controller/getting-started/">APISIX Ingress Controller</a> 则完美解决了上面提到的所有问题:支持全动态,无需重启加载。同时继承了 APISIX 的所有优势,还支持原生 Kubernetes CRD,方便用户迁移。</p> <p><img src="https://static.apiseven.com/2022/blog/0729/6.png" alt="APISIX Kubernetes Ingress" referrerpolicy="no-referrer"></p> <h3>服务网格</h3> <p>未来五到十年,基于云原生模式架构下的服务网格架构开始崭露头角。APISIX 也提前开始锁定赛道,通过调研和技术分析后,APISIX 已经支持了 xDS 协议,APISIX Mesh 就此诞生,在服务网格领域 APISIX 也拥有了一席之地。</p> <p><img src="https://static.apiseven.com/2022/blog/0729/7.png" alt="APISXI Mesh" referrerpolicy="no-referrer"></p> <h2>总结</h2> <p>Apache APISIX 自开源第一天,迄今为止已经有三年的时间了,高活跃度的社区以及实际的<a href="https://apisix.apache.org/zh/blog/tags/case-studies/">用户案例</a>证明了 APISIX 是云原生时代最完美的 API 网关。通过阅读本文,相信你已经对 APISIX 有了更全面的认识,期待你在生产环境中使用 APISIX 作为你的 API 网关。</p> <p>欢迎填写<a href="https://apiseven.mikecrm.com/pvdVjd5">试用表单</a>,体验基于 Apache APISIX 的企业产品。</p>

Apache APISIX 在 API 和微服务领域的探索

<h2>背景介绍</h2> <p>Apache APISIX 是一个动态、实时、高性能的开源 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。</p> <p>作为 API 网关,Apache APISIX 可以帮助企业快速、安全地处理 API 和微服务流量,应用于网关、Kubernetes Ingress 和服务网格等场景。利用 APISIX 既可以处理从客户端到服务端的南北向流量,也可以处理从各个企业微服务之间的东西向流量。</p> <p>APISIX 在 2019 年 6 月 6 号开源,同年 10 月份捐赠给了 Apache 软件基金会。在之后不到一年的时间内,毕业成为 Apache 顶级项目。</p> <p>在项目快速发展的背后,APISIX 在技术上进行了哪些探索?为什么会得到越来愈多开发者和企业用户的青睐?在接下来的内容中,将为你讲述更多细节。</p> <h2>APISIX 技术探索之路</h2> <h3>摆脱数据库依赖</h3> <p>在 APISIX 项目问世之前,也有非常多的商业 API 网关或开源 API 网关产品,但这些产品大多数都把 API 数据、路由、证书和配置等信息存放在一个关系型数据库中。</p> <p>存放在关系型数据库的优势其实很明显,可以让用户更加方便地使用 SQL 语句进行灵活查询,方便用户进行备份及后续维护等环节。</p> <p>但这种情况也会额外带来一个问题。网关作为一个基础中间件,它处理了所有来自客户端的流量,这种情况下对于可用性的要求便会非常高。如果你的 API 网关依赖了一个关系型数据库,也就意味着关系型数据库一旦出现了故障(比如宕机、丢失数据),你的API网关也会因此受到影响。这种情况下,系统的整体可用性就会大打折扣。</p> <p>所以 APISIX 在设计之初,就从底层架构上避免了类似上述情况的发生。</p> <p><img src="https://static.apiseven.com/2022/blog/0722/apisix-1.png" alt="APISIX 架构" referrerpolicy="no-referrer"></p> <p>APISIX 的架构主要分成两部分。第一部分叫做数据面,它是真正去处理来自客户端请求的一个组件,去处理用户的真实流量,包括像身份验证、证书卸载、日志分析和可观测性等功能。数据面本身并不会存储任何数据,所以它是一个无状态结构。</p> <p>第二部分叫做控制面。APISIX 在底层架构上和其它 API 网关的一个很大不同就在于控制面。APISIX 在控制面上并没有使用传统的类似于像 MySQL 去做配置存储,而是选择使用 etcd。这样做的好处主要有以下几点:</p> <ol> <li>与产品架构的云原生技术体系更统一</li> <li>更贴合 API 网关存放的数据类型</li> <li>能更好地体现高可用特性</li> <li>拥有低于毫秒级别的变化通知</li> </ol> <p>使用 etcd 后,对于数据面而言只需监听 etcd 的变化即可。如果轮询数据库的话,可能需要 5-10 秒才能获取取到最新的配置;但如果监听 etcd 的配置变更,就可以将时间控制在毫秒级别之内,达到实时生效的效果。</p> <p>所以使用 etcd 而不是关系型数据库,不仅让 APISIX 在底层上更加贴合云原生,也让它在系统高可用的体现上带来了更多优势。</p> <h3>支持多语言插件</h3> <p>API 网关其实和数据库或其他中间件不太一样,虽然都属于基础组件,但对于网关来说,它更多使用场景是进行一些定制化开发和系统集成。</p> <p>目前 APISIX 官方虽然有非常多的插件,但仍难以涵盖用户的所有使用场景。所以在真实使用场景中,多多少少都会面对业务进行一些定制化的插件开发。通过网关去集成更多的协议或者系统,最终实现在网关层的统一管理。</p> <p>APISIX 在刚开始只支持使用 Lua 语言进行开发插件。这样做的好处在于,通过原生计算语言的底层优化,可以让开发出来的插件具备非常高的性能。但是它的劣势也非常明显,就是学习 Lua 这门新语言是需要时间和理解成本的。</p> <p>为此,APISIX 通过两种方式去解决了上述问题。</p> <p>第一种方式就是通过 Runner Plugin 来支持更多的主流开发语言,比如 Java、Python、Go 等。如果你是一个后端工程师,至少应该会其中一种语言,那么这个时候你就可以非常方便地通过本地 RPC 通信,使用你之前熟悉的计算语言去开发一个 APISIX 插件。</p> <p>这样做的好处是减少了开发成本,提高了开发效率。当然弊端就是在性能层面有一些损失。那么,有没有一种既能达到 Lua 原生性能,同时又兼顾高级语言的开发效率方案呢?</p> <p><img src="https://static.apiseven.com/2022/blog/0722/apisix-2.png" alt="APISIX 多语言架构" referrerpolicy="no-referrer"></p> <p>这里就引出了第二种方式,也就是上图左侧部分。WebAssembly 最早是用在前端或浏览器上的一个技术,在服务端它也逐渐展示出来它的优势。</p> <p>把 WebAssembly 嵌入到了 APISIX 里,用户就可以使用 WebAssembly 去编译成 WebAssembly 的字节码在 APISIX 中运行。最终达到的效果就是利用高效率,开发出了一个既有高性能又使用高级计算语言编写的 APISIX 插件。</p> <p>所以在目前的 APISIX 版本中,用户可以使用 Lua、Go、Python 和 Wasm 等多种方式,基于 APISIX 编写自定义代码。通过这样的方式,降低了开发者的使用门槛,也为 APISIX 的功能提供了更多的可能性。</p> <h3>插件热加载</h3> <p>APISIX 和 NGINX 相比,有两处非常大的变化:APISIX 支持集群管理和动态加载。</p> <p>如果大家使用过 NGINX,就知道它的所有配置都会写在 nginx.conf 这个配置文件中。如果想要进行集群控制,就需要一台台去修改它的 nginx.conf 文件。整个过程中没有一个集中化管理的控制平面,每一个 NGINX 都是一个数据面+控制面的混合体。如果此时你有几十台几百台 NGINX,它的管理成本就会特别高。</p> <p>在上述场景中,修改完每台 NGINX 的 nginx.conf 文件后,都需要重启才能生效。比如进行证书更新或者上游变更,都需要先修改配置文件,然后重启生效。如果你的请求不是特别多,那么这种方法勉强还能接受。但随着越来越多 API 和微服务的调用,如果每次修改都进行重启,这对于客户端的波动其实会非常大。</p> <p><img src="https://static.apiseven.com/2022/blog/0722/apisix-6.png" alt="动态加载列表" referrerpolicy="no-referrer"></p> <p>上图中列出来了 APISIX 目前在哪些组件中实现了动态加载,从这个列表我们可以看到从上游到证书,甚至插件本身,它的代码都是实时生效的。那么其实在社区里面就会有人问到,他可以理解上游、证书这些是动态的,因为这些会经常变化,但为什么插件的修改也要做成动态的呢?因为插件的修改并不是一个特别频繁的操作,没有必要做成一个极致的动态。</p> <p>对于 APISIX 的底层设计工程师来说,我们希望它可以做到一个极致的动态。因为极致动态带来了一个非常大的优势就是增加了更多可能性。比如说用户可以在不修改任何插件代码的情况下,对已有代码进行故障排查,这个过程中可能会存在调试插件的修改。那么这种情况下,用户就可以不用重启,继而随时复现问题并记录。这种调试功能的插件配合插件热加载的机制后,就会变得非常灵活,帮助开发者在排查问题的过程中省时省力。</p> <h3>插件动态编排</h3> <p>除了上述提到的插件热加载,APISIX 在插件和插件之间也支持实时的动态编排,动态编排也为插件的运行带来了无限可能。</p> <p>什么叫插件编排?我们在提出各种各样的需求时,更多是希望可以把一个需求变成一个插件,就像玩乐高一样,通过一个统一的标准(形状契合、交叉等),就可以搭出来无限种可能的造型,这也是乐高的乐趣之一。那么对于 APISIX 的插件来说,每一个插件其实都完成了一个独立的场景需求,那么有没有一种可能,可以让用户像搭乐高一样,把各种插件摆出来让用户自己按照需求进行排列组合呢?</p> <p>比如 APISIX 现在提供了 100 个插件,那么 APISIX 给用户暴露的功能其实也就只有这 100 个插件所具有的功能,并没有把底层的一些灵活性展露出来。在进行技术中间件开发时,我们不仅需要考虑这个产品现在能做成什么样子,而更应该考虑的是未来用户在上手使用时,能否将这个产品赋予更多可能性。</p> <p>APISIX 目前有将近 100 个插件,在加入插件编排能力后,你会发现它的可能性不是 100 种,而是100×99×98×97×96x……,也就是接近无限种可能。</p> <p>举个例子,比如你想对一个用户进行限流限速,当他被限速之后,一般情况下是返回一个错误码。这时你是否可以尝试对接一个日志记录插件,或者是错误上报插件来进行后续的活动记录。下方图片展示了 APISIX 的插件编排模式。</p> <p><img src="https://static.apiseven.com/2022/blog/0722/apisix-3.png" alt="插件编排" referrerpolicy="no-referrer"></p> <p>这个功能其实隐藏了一个很大的好处,每一个插件的代码都被完整的测试案例覆盖到。也就是说当用户进行插件编排时,可以不用写任何代码。这对于像产品经理、安全工程师以及像运维工程师来说,不需要投入专门的学习成本,只需要拖拽一些插件然后设置一些条件,就可以诞生一个专属自己的 APISIX 新插件,同时这个新插件的代码质量是和开源 APISIX 的官方代码质量一样高。</p> <h3>全流量网关</h3> <p>对于服务端的工程师来说,如果你进行一些和网关相关的开发,基本都会涉及到两个概念。一个是南北向流量,指的是从客户端、浏览器或 IoT 设备等到达服务端的流量,这个流量属于纵向的。另一个就是东西向流量,指的是在企业内部,系统与微服务之间的互相调用,这个流量属于横向的。</p> <p>在处理纵向和横向的这些流量时,各组件组成也会有所不同。比如在处理南北向流量的组件中,可能会先通过一个 LB,然后再到网关,通过网关之后可能会进入一个业务网关。所以就会有类似于像 NGINX、APISIX、Sping Cloud Gateway 这样的组件。那么在东西向流量中,如果你使用了服务网格,那么可能会用到类似于像 Envoy 这样的组件,这些组件看上去会非常多,但如果仔细观察它们的功能,你会发现这些组件基本上都是一样的,大多都是进行像路由调度、动态上游以及安全身份认证的插件实现等。</p> <p>所以这种情况下,能否把处理南北向与东西向流量的组件统一起来?理想状态是当一个客户端的请求进到服务端之后,全部由 APISIX 来处理。即不管流量是南北向还是东西向,都是通过控制面去控制所有流量与数据。这在 APISIX 目前的技术探索中,是完全可以实现的。</p> <p>在实际使用过程中,通过一些现有用户的实践反馈,你会发现这种模式能够大大降低用户自身的运维成本,同时可以降低整体系统的复杂度,从而提升了整个系统之间的响应速度。这样的反馈结果,也让后续 APISIX 的迭代有了更清晰的方向,让 APISIX 在全流量网关层面去尝试更多的功能和角色。</p> <h3>多服务发现组件</h3> <p>网关虽然是基础组件,但它的位置极其重要。它会处理所有来自客户端的请求,处理完成后,对于 API 网关来说还有一个非常重要的职责,就是去和各种各样的系统及开源项目进行集成。</p> <p>在集成过程中,就会用到一个非常重要的组件——服务发现与注册。因为用户会把各种各样的服务放置到 Eureka、Nacos 这样单独的组件中去完成。对于一些大规模或业务存续较久的 IT 系统来说,多个服务发现组件并存是非常常见的。</p> <p>这种情况下,其实所有的流量出入口都是网关。你可能需要在A路由上去单独指定一个 Nacos 服务发现,在B路由上指定 Consul 的服务发现等。这对于绝大部分的网关来说,一般都只支持一个服务发现,这时你可能就要部署多套网关,让不同的网关对接不同的服务发现组件。</p> <p>目前在 APISIX 中不仅支持在数据面的服务发现,也逐步支持在控制面上去对接多个服务注册和发现的组件。这对于一些大规模和业务久远的企业来说,就是一个非常好的解决方案,只部署一个 API 网关,就可以轻松对接多种不同的服务发现和注册组件。</p> <h3>多云与混合云场景探索</h3> <p>对于网关来说,当用户把它部署到生产环境中时,如果是云原生架构,那么多云和混合云必然是一个长期存在的技术场景。对于 APISIX 来说,当它拥有了完善的功能、性能、插件和多服务发现后,就不可避免地去考虑如何让用户在生产环境中更好地去运行起来。</p> <p>多云和混合云场景其实对 APISIX 带来了更多的挑战,需要考虑更多细节。</p> <ol> <li> <p><strong>上下游均支持 mTLS</strong></p> <p>之前我们觉得支持上游的 mTLS 这个功能的优先级并不高,但一旦处于跨云场景,上游可能就是另外一个云上的服务,或者另外一个 SaaS 服务等。为了提高数据的安全性,就需要进行 mTLS 功能的加持。</p> </li> <li> <p><strong>控制面与数据面架构完全分离</strong></p> <p>APISIX 在最近一年中被爆出来几个安全漏洞,这些安全漏洞的来源很多是因为 APISIX 的控制面与数据面是混合部署在一起的。可以理解为 APISIX 服务启动完成之后,它的控制面与数据面在同一个服务里。此时如果一个黑客通过安全漏洞入侵了某个数据面,那么也就意味着他有机会能够入侵到控制面,从而控制所有的数据面,造成非常大的影响。</p> <p>在 APISIX 后续的架构设计中,我们在考虑把数据面与控制面完全分离。采用不同的端口与服务,将其部署在完全不同的服务器上,从而避免出现上述安全隐患。</p> </li> <li> <p><strong>加强安全管理</strong></p> <p>网关一般会存储一些比较敏感的数据,比如有些用户可能就会直接把 SSL 证书存储在网关上,或者把连接 etcd 的密钥信息存在网关上。这种情况下一旦 etcd 被侵入了或者数据面被攻破,就有可能造成比较严重的数据泄密。</p> <p>因此就需要考虑在存储一些关键信息时,去支持使用 Vault 这样专门存密钥的组件来进行敏感信息的保护。这样做不仅可以让 APISIX 自身更加安全,也能够让用户使用 APISIX 时更加合规。</p> </li> <li> <p><strong>集成更多云上标准</strong></p> <p>在多云情况下我们需要去考虑,APISIX 在类似阿里云、腾讯云及 AWS 等云服务商中使用时,如何与他们更好地融合在一起来使用?我们的初衷是希望用户不用配置任何东西就能够在各个云平台上顺利运行起来。</p> <p>这个过程的实现,并不说让用户再进行自定义插件配置,而是直接让 APISIX 去集成各个云上的标准、 API 或其他服务,提前帮用户做好适配,保证后续用户的直接上手体验感。</p> </li> </ol> <h2>前行之路的支持者</h2> <p>纵观 APISIX 的发展历程,其实在技术层面做了非常多的创新。不管是从一开始的代码搭建,还是目前仍然保持的快速迭代,越来越多的社区贡献者们携手并进,将 APISIX 逐渐搭建成了完整且功能日渐丰富的API网关。</p> <p>一个开源产品的迭代与进步,必然少不了贡献者与使用者的功劳。</p> <p>在 APISIX 刚捐赠给 Apache 软件基金会时,它还并不是一个特别成熟的项目,那时的 APISIX 只有 20 个贡献者。如今的迅速发展,也让 APISIX 在全球范围内收获众多使用者、贡献者和企业用户。比如 WPS、新浪微博、爱奇艺等国内大厂,这些都是每天承载几百亿次 API 调用的企业用户。 在海外也有像 NASA、欧盟数字工厂、瑞士电信等非常多的一些用户在使用。</p> <p><img src="https://static.apiseven.com/2022/blog/0722/apisix-4.png" alt="APISIX 企业用户" referrerpolicy="no-referrer"></p> <p>拿 WPS 这种云办公软件来说,不管你在手机、浏览器还是终端设备上进行多人协同办公时,几十甚至几百个人可以同时去编辑一份文档,同时你可以实时查看到其他人进行的修改。这个功能实现的背后,就是通过 API 的各种调用来实现的。</p> <p>国内这些大厂企业用户的使用场景中,多是处理上百亿 API 调用和峰值 QPS 超百万的规模。这种使用规模下,也让 APISIX 真正在大规模场景中得到历练与收获真实场景反馈。得益于这些企业用户的场景使用,APISIX 作为开源产品才得以更成熟快速发展。</p> <p>当然除了将 APISIX 应用到企业业务中来,很多使用者在使用 APISIX 的过程中,也会把他们的一些经验或者代码功能迭代等反馈给社区,实现了双方互利共赢的趋势。这也说明使用者们不仅觉得 APISIX 是一个好的产品,更是一个值得去参与其中的开源项目。先赢得开发者们的喜欢,才有机会去变成一个真正有价值的开源项目。</p> <p>贡献者们用体验和反馈打造了非常多的产品功能,使用者们利用这些功能又可以给企业带来价值。这个才是真正开源的精髓所在,而不是一味只去追求各种表面数据而带来的数字繁荣。</p> <h2>APISIX 3.0 前瞻</h2> <p>至此我们聊了非常多关于 APISIX 在过去及现在进行的探索。从时间线来看,可以把 APISIX 的发展历程分为几个阶段。</p> <p>APISIX 1.0 阶段,是在打造这个产品的框架,将底层架构打理好并呈现一些 API 网关的基本功能。来到 2.0 阶段,则是在初始版本上进行了更细致的探索,让底层更加灵活,让架构更加丰满。</p> <p>对于一个成熟的开源项目来说,它成熟的标志并不是说功能有多强大,而是在产品使用上,为用户和开发者带来更好的使用体验。</p> <p>在目前阶段中,APISIX 都是在技术上进行了非常多的探索与创新,但并未考虑到使用者背后的感受,比如文档质量参差不齐、教学视频少之又少等现状。这就让很多用户在刚接触 APISIX 时,存在茫然与不知所措。不知道怎么使用,不知道如何将某些功能应用到什么场景,不知道 APISIX 这些强大的功能背后能给企业带来怎样的独特价值。</p> <p>所以在接下来的 APISIX 3.0 阶段中,会尝试解决类似的问题,重构掉目前很多对于开发者体验不友好的地方。比如重新去设计 API,去掉对于 etcd 的一些特殊返回值的依赖;重构官方使用文档,让文字性这种最直接的表达方式对开发者更加友好。在功能层面,也会进行控制面与数据面分离,让 APISIX 本身变得更加安全;支持更多的 4 层协议,支持更多的 RPC 协议,让用户可以非常方便地去实现南北东西向的全流量网关。</p> <p><img src="https://static.apiseven.com/2022/blog/0722/apisix-5.png" alt="APISIX 3.0 功能一览" referrerpolicy="no-referrer"></p> <p>这些待实现功能加在一起,你会发现 APISIX 3.0 阶段将会变得比之前版本更加安全可靠与易用。功能强大的同时,仍不忘使用体验。我们希望未来的 APISIX,是可以处理全球所有 API 和微服务的请求,最终达到一个非常好的使用价值,助力企业在 API 和微服务流量的处理上更加便利。</p> <p>预计在 2022 年年底,APISIX 将会发布全新的 3.0 版本,希望大家也可以持续关注 APISIX 新版本的各种动向,踊跃参与 Apache APISIX 项目共建。</p>

API 网关 Apache APISIX 集成 Eureka 作为服务发现"

<p>微服务架构中,大型复杂的系统按功能或者业务需求垂直切分成更小的子系统,这些子系统以独立部署的子进程存在,它们之间通过网络调用进行通信。这些独立部署的服务如何发现对方成为了首先要解决的问题,所以在微服务架构中往往都会存在一个中心化的注册中心。</p> <p>Spring 作为 Java 生态中最核心的开发框架,从 Spring MVC 到 Spring Boot 持续不断解放着 Java 开发者的生产力,而 Spring Cloud 是 Spring 面向云原生时代微服务架构给出的答案。</p> <p>在 Spring Cloud 中,Eureka 就扮演了注册中心的角色。Eureka 是一款由 Netflix 开源,使用 Java 语言编写的注册中心服务,其在 Netflix 的基础设施中扮演着重要角色。</p> <p>Apache APISIX 是一个动态、实时、高性能的 API 网关,提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。Apache APISIX 作为业界领先的微服务 API 网关,对 Eureka 提供了原生支持。本文将会使用 Spring Cloud 演示项目作为案例,为大家展示 Apache APISIX 对接 Eureka 服务发现的主要功能及特性。</p> <h2>准备阶段</h2> <p>本次演示使用 Spring 官方提供的 <a href="https://spring.io/projects/spring-cloud-netflix#overview"><code>spring-cloud-netflix</code></a> 教程作为示例,该教程中提供了使用 SpringBoot 启动的 Eureka Server 作为 Spring Cloud 的注册中心,我们也使用相同的方式来启动用于演示的 Eureka 服务端。该项目地址请访问 <a href="https://github.com/spring-cloud-samples/eureka"><code>spring-cloud-samples/eureka</code></a>。</p> <p>接下来将为您介绍相关代码和启动方式。</p> <h2>Eureka Server</h2> <p>Spring Cloud 为 Eureka 提供了一个 <code>EnableEurekaServer</code> 的注解,可以直接以 Spring Boot 的方式启动一个 Eureka Server。</p> <p>代码示例如下:</p> <pre><code class="language-Java">@SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class,args); } } </code></pre> <p>启动方式可直接参考下方代码:</p> <pre><code class="language-Shell">git clone git@github.com:spring-cloud-samples/eureka.git # 在项目根目录执行 ./gradlew bootRun </code></pre> <p><code>resources</code> 目录中的 <code>application.yml</code> 文件定义了 Eureka Server 监听在 <code>8761</code> 端口。</p> <pre><code class="language-YAML">server: port: 8761 </code></pre> <h3>接入 Eureka Client 的 HTTP 服务</h3> <p>与 <code>EnableEurekaServer</code> 对应的客户端注解是 <code>EnableEurekaClient</code>,使用 <code>EnableEurekaClient</code> 可以非常简单得将一个使用 Spring Boot 启动的 HTTP 应用注册到 Eureka 上。</p> <p>以下为示例代码:</p> <pre><code class="language-Java">@SpringBootApplication @EnableEurekaClient @RestController public class Application { @Value("${server.port}") int serverPort; @RequestMapping("/") public String home() { return String.format("server-%s",serverPort); } public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args); } } </code></pre> <p>这里我们在 <code>/</code> 路径上暴露一个 HTTP 服务,返回当前 Spring Boot 使用的端口,这样我们可以使用不同的配置文件,启动多个实例,来演示 APISIX 对注册到 Eureka 的服务端实例列表进行负载均衡的效果。</p> <p>配置文件如下:</p> <pre><code class="language-JAVA">spring.application.name=a-bootiful-client #将会作为注册到 Eureka 中的 application name server.port=8080 # 修改监听端口以启动多个实例 </code></pre> <p>设置监听端口为 <code>8080</code>,<code>8081</code>,<code>8082</code>,并启动三个 Spring Boot 实例,完成后,使用浏览器访问 Eureka Server 的 <code>8761</code> 端口,可以查看服务注册的结果。</p> <p><img src="https://static.apiseven.com/202108/1646350535070-7615a784-df05-4e94-a88e-b039c111de53.png" alt="error/results example.png" referrerpolicy="no-referrer"></p> <p>您可以看到应用 <code>A-BOOTIFUL-CLIENT</code> (注意:spring.application.name 被全部转为大写字符)下注册了三个实例,分别暴露了 <code>8080</code>,<code>8081</code>,<code>8082</code> 端口,并且均处于 UP 状态。</p> <h2>使用 APISIX 代理 SpringCloud 应用</h2> <p>接下来,我们将会实现如下图所示的请求链路:</p> <p><img src="https://static.apiseven.com/202108/1646350644536-7b68a4a3-b523-4c82-8e19-822624ff2c95.png" alt="error/request link.png" referrerpolicy="no-referrer"></p> <h3>启动 Apache APISIX</h3> <p>首先,需要在 Apache APISIX 的配置文件 <code>config.yaml</code> 中找到 <code>apisix.discovery</code>,修改 Eureka Server 连接信息的相关配置,并启动 APISIX。</p> <pre><code class="language-YAML"> discovery: # service discovery center eureka: host: # it's possible to define multiple eureka hosts addresses of the same eureka cluster. - "http://172.23.79.129:8761" # Access address of Eureka Server started by Spring Boot prefix: /eureka/ fetch_interval: 30 # default 30s weight: 100 # default weight for node timeout: connect: 2000 # default 2000ms send: 2000 # default 2000ms read: 5000 # default 5000ms </code></pre> <h3>创建路由</h3> <p>创建一个 Route,并在 Upstream 中配置启用 Eureka Service Discovery 插件。</p> <ul> <li><code>upstream.discovery_type</code> 为 <code>eureka</code>。</li> <li><code>upstream.service_name</code> 是应用在 Eureka 中注册的应用名 <code>A-BOOTIFUL-CLIENT</code>。</li> </ul> <pre><code class="language-Shell">curl http://172.30.45.72:9180/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' "uri": "/*", "host": "eureka-demo", "upstream": { "service_name": "A-BOOTIFUL-CLIENT", "type": "roundrobin", "discovery_type": "eureka" } }' </code></pre> <h3>请求路由</h3> <p>使用 <code>curl</code> 命令进行多次请求,验证负载均衡效果。</p> <pre><code class="language-Shell">$ curl http://172.30.45.72:9080/ -H "Host: eureka-demo" server-8081% $ curl http://172.30.45.72:9080/ -H "Host: eureka-demo" server-8080% $ curl http://172.30.45.72:9080/ -H "Host: eureka-demo" server-8082% $ curl http://172.30.45.72:9080/ -H "Host: eureka-demo" server-8081% $ curl http://172.30.45.72:9080/ -H "Host: eureka-demo" server-8080% $ curl http://172.30.45.72:9080/ -H "Host: eureka-demo" server-8082% </code></pre> <p>从上述返回结果可以看到,请求被依次分配到 Eureka 中注册的三个实例上,这是因为我们使用的负载均衡算法是 <code>roundrobin</code>,所有的后端实例会被轮流分配请求。</p> <h3>模拟实例宕机</h3> <p>关闭其中 <code>8081</code> 实例,模拟实例宕机场景,观察请求效果。</p> <pre><code class="language-Shell">$ while true; do curl http://172.30.45.72:9080/ -H "Host: eureka-demo"; echo; sleep 1; done server-8080 server-8082 server-8081 server-8080 server-8082 server-8081 server-8080 server-8082 server-8080 server-8082 </code></pre> <p>由上述结果可以看出,关闭 <code>8081</code> 实例后,Apache APISIX 会及时同步到 Eureka 的最新实例列表,然后将请求转发给正确的后端。</p> <h3>诊断工具</h3> <p>在微服务系统中,经常会遇到非预期转发的问题,这些问题的原因可能来自服务发现中的各个链路,例如:客户端注册异常,注册中心本身数据异常,网关读取注册数据异常等等,因此发生异常时链路中可以使用的诊断工具将会尤为重要。</p> <p>所以 APISIX 在 Service Discovery 插件中提供了一个诊断接口,可以方便的查询出当前网关正在使用的服务列表,结合注册中心的控制台,我们就对网关到注册中心这一链路做出快速诊断。</p> <p>诊断接口默认暴露在回环接口的 <code>9090</code> 端口,访问方式为 <code>GET /v1/discovery/{discovery_type}/dump</code>,例:</p> <pre><code class="language-Shell">curl http://localhost:9090/v1/discovery/eureka/dump { "services": { "A-BOOTIFUL-CLIENT": [ { "weight": 100, "metadata": { "management.port": "8081" }, "host": "192.168.50.164", "port": 8081 }, { "weight": 100, "metadata": { "management.port": "8080" }, "host": "192.168.50.164", "port": 8080 }, { "weight": 100, "metadata": { "management.port": "8082" }, "host": "192.168.50.164", "port": 8082 } ] }, "config": { "prefix": "\/eureka\/", "timeout": { "connect": 2000, "send": 2000, "read": 5000 }, "fetch_interval": 30, "host": [ "http:\/\/172.23.79.129:8761" ], "weight": 100 } } </code></pre> <p>通过上述示例就可以查询到 APISIX 正在使用的 Eureka 数据。</p> <h2>总结</h2> <p>Spring Cloud 是一个广受用户欢迎的微服务框架,而 Apache APISIX 通过支持 Eureka Service Discovery 提供了处理 Spring Cloud 应用流量的能力,我们可以看到这两个生态的紧密结合,让微服务架构的落地变得更加简单高效,从而让业务开发可以更加的专注于业务价值。</p> <p>关于 <code>eureka</code> 插件的更多说明和完整配置信息,可参考 <a href="https://apisix.apache.org/zh/docs/apisix/discovery#%E5%A6%82%E4%BD%95%E6%89%A9%E5%B1%95%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83%EF%BC%9F">Apache APISIX 官网文档</a>。</p> <p>Apache APISIX 项目目前正在开发其他插件以支持集成更多服务,如果您对此有兴趣,您可以通过 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 发起讨论,或通过<a href="https://apisix.apache.org/docs/general/subscribe-guide">邮件列表</a>进行交流.</p>

化繁为简,Apache APISIX 集成 ClickHouse 插件提升全链路日志效率

<p>本文作者祁振东,来自中国移动云能力中心。从事分布式对象存储软件开发工作,参与移动云多个资源池的建设工作,在对象存储领域有丰富的实战经验。本文讲述了社区贡献者祁振东为 Apache APISIX 贡献 <code>clickhouse-logger</code> 的历程,以及如何使用该插件简化业务架构,提升全链路日志效率。</p> <h2>背景信息</h2> <p>Apache APISIX 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。作为 API 网关,Apache APISIX 拥有多种类型的实用插件。得益于 Apache APISIX 的插件热加载机制,我们无需重启便可启用插件、修改插件配置和停用插件。</p> <p>ClickHouse 由 Yandex 开发,在 2016 年开源。ClickHouse 不止是一个数据库, 也是一个数据库管理系统,它允许在运行时创建表和数据库、加载数据和运行查询,而无需重新配置或重启服务。</p> <p>随着越来越多的公司开始将业务迁移上云,如何高效地实现日志收发及日志分析,增强系统的可观测性成为了一个难题。中国移动云能力中心作为一家提供公有云服务的企业,前期基于 Apache APISIX 的业务日志收发和和分析系统的架构大致是这样的。</p> <p><img src="https://static.apiseven.com/202108/1646363723740-f92d6a39-64e0-4464-8c44-c73832362bf6.png" alt="initial business architecture" referrerpolicy="no-referrer"></p> <p>随着业务的增长,上述方案不仅维护成本奇高,而且难以满足我们对于精细化数据分析的需求。由于 rsyslog 接收到的数据是一个字符串,而不是 JSON 格式的日志,给日志分析带来了一定的难度。</p> <p>计算机领域有句名言: “任何问题都可以通过增加一个间接的中间层来解决”。我们其实也考虑过在 <code>tcp-logger</code> 和 rsyslog 之间再加一个中间层,使字符串转换为 JSON。但这显然不是长久之计。</p> <p>所以我们换了一种思路去看待这个问题:如果把现有架构中的“tcp-logger+rsyslog+Promtail+Loki”看作是一个巨大的中间层,那么不论我们在这中间怎么添加额外的中间层,除了能解决燃眉之急外,只会使它变得更加臃肿和难以维护。市面上有没有一个产品能直接把“tcp-logger+rsyslog+Promtail+Loki”给替换掉呢?</p> <p>带着这个问题,我们花了些时间进行调研,最终选择 ClickHouse 主要有以下几点原因。</p> <ol> <li>ClickHouse 提供 HTTP 接口,方便其它模块调用。</li> <li>基于 ClickHouse 的分析工具链很成熟,能够满足我们对日志分析的需求。</li> <li>ClickHouse 支持使用对象存储作为存储引擎,非常方便。</li> <li>没有必要自己重复“造轮子”。</li> </ol> <p>接下来就只剩下一个问题需要解决了:如何实现 Apache APISIX 和 ClickHouse 之间的对接?以插件的形式实现对接其实是一个不错的方法。作为 Apache APISIX 社区的一员,我一直都在社区里面“潜水”,看到了最近 Apache APISIX 在生态方面的持续进步,其实我也有些心动,一直在使用 Apache APISIX,但还没有给社区贡献过代码,不如就借这个机会为社区的生态发展添一把火吧。</p> <h2>ClickHouse 插件实现原理</h2> <p><code>clickhouse-logger</code> 插件的作为一个中间层,对接 Apache APISIX 和 ClickHouse。如前文所说,我们使用Apache APISIX 作为七层负载均衡,请求经过 Apache APISIX 会产生日志,比如 access log 和 error log。<code>clickhouse-logger</code> 收集到日志后,会按照自身 metadata 所设置的日志格式,对这些日志进行整理。最后依靠批处理器将整理过的日志批量发送至 ClickHouse。</p> <p><img src="https://static.apiseven.com/202108/1646363936994-c2646095-1ea4-4c1f-8cad-1dcecfc41df3.png" alt="clickhouse-logger architecture" referrerpolicy="no-referrer"></p> <p><code>clickhouse-logger</code> 在我们这个场景下,起到了替代“tcp-logger+rsyslog+Promtail+Loki”的作用。免除了多个组件之间的格式转换和数据转发,可将 Log 数据请求直接推送到 ClickHouse 服务器。</p> <p><img src="https://static.apiseven.com/202108/1646364005040-93d70286-e7e6-4fb5-a164-1de1c865ce2b.png" alt="improved business architecture" referrerpolicy="no-referrer"></p> <h2>操作步骤</h2> <p>以下是在一个路由中启用 <code>clickhouse-logger</code> 插件的示例过程。</p> <h3>启用 ClickHouse 插件</h3> <p>运行 <code>curl</code> 命令,为指定路由开启 <code>clickhouse-logger</code> 插件。</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { "clickhouse-logger": { "user": "default", "password": "a", "database": "default", "logtable": "test", "endpoint_addr": "http://127.0.0.1:8123" } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } }, "uri": "/hello" }' </code></pre> <p><code>clickhouse-logger</code> 的参数表如下。</p> <table> <thead> <tr> <th>名称</th> <th>类型</th> <th>必填</th> <th>默认值</th> <th>取值范围</th> <th>描述</th> </tr> </thead> <tbody> <tr> <td>endpoint_addr</td> <td>string</td> <td>是</td> <td>n/a</td> <td>n/a</td> <td>ClickHouse 服务器的 endpoint。</td> </tr> <tr> <td>database</td> <td>string</td> <td>是</td> <td>n/a</td> <td>n/a</td> <td>使用的数据库。</td> </tr> <tr> <td>logtable</td> <td>string</td> <td>是</td> <td>n/a</td> <td>n/a</td> <td>写入的表名 。</td> </tr> <tr> <td>user</td> <td>string</td> <td>是</td> <td>n/a</td> <td>n/a</td> <td>ClickHouse 的用户。</td> </tr> <tr> <td>password</td> <td>string</td> <td>是</td> <td>n/a</td> <td></td> <td>ClickHouse 的密码 。</td> </tr> <tr> <td>timeout</td> <td>integer</td> <td>否</td> <td>3</td> <td>[1,...]</td> <td>发送请求后保持连接活动的时间。</td> </tr> <tr> <td>name</td> <td>string</td> <td>否</td> <td>"clickhouse-logger"</td> <td>n/a</td> <td>标识 logger 的唯一标识符。</td> </tr> <tr> <td>batch_max_size</td> <td>integer</td> <td>否</td> <td>100</td> <td>[1,...]</td> <td>设置每批发送日志的最大条数,当日志条数达到设置的最大值时,会自动推送全部日志到 <code>clickhouse</code>。</td> </tr> <tr> <td>max_retry_count</td> <td>integer</td> <td>否</td> <td>0</td> <td>[0,...]</td> <td>从处理管道中移除之前的最大重试次数。</td> </tr> <tr> <td>retry_delay</td> <td>integer</td> <td>否</td> <td>1</td> <td>[0,...]</td> <td>如果执行失败,则应延迟执行流程的秒数。</td> </tr> <tr> <td>ssl_verify</td> <td>boolean</td> <td>否</td> <td>true</td> <td>[true,false]</td> <td>验证证书。</td> </tr> </tbody> </table> <h3>测试 ClickHouse 插件</h3> <ol> <li>使用 <code>curl</code> 命令测试插件。</li> </ol> <pre><code class="language-shell">curl -i http://127.0.0.1:9080/hello </code></pre> <ol start="2"> <li>返回结果如下,则表示成功启用。</li> </ol> <pre><code class="language-shell">HTTP/1.1 200 OK ... hello, world </code></pre> <h3>进阶操作 1:设置日志格式</h3> <p>你可以使用 <code>log_format</code> 这个元数据设置自定义的日志格式,示例如下。</p> <ol> <li>配置 <code>log_format</code> 元数据参数。</li> </ol> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/clickhouse-logger \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "log_format": { "host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr" } }' </code></pre> <blockquote> <p>以 JSON 格式的键值对来声明日志格式。对于值部分,仅支持字符串。如果是以 <code>$</code> 开头,则表明是要获取 <a href="https://apisix.apache.org/docs/apisix/apisix-variable">APISIX 变量</a>或 <a href="http://nginx.org/en/docs/varindex.html">Nginx 内置变量</a>。<strong>该设置是全局生效的</strong>,意味着指定 <code>log_format</code> 后,将对所有绑定 <code>http-logger</code> 的 Route 或 Service 生效。</p> </blockquote> <ol start="2"> <li>创建 ClickHouse 写入的表格。</li> </ol> <pre><code class="language-sql">CREATE TABLE default.test ( `host` String, `client_ip` String, `route_id` String, `@timestamp` String, PRIMARY KEY(`@timestamp`) ) ENGINE = MergeTree() </code></pre> <ol start="3"> <li>在 ClickHouse 上执行 <code>select * from default.test;</code>,将得到类似下面的数据。</li> </ol> <pre><code class="language-shell">┌─host──────┬─client_ip─┬─route_id─┬─@timestamp────────────────┐ │ 127.0.0.1 │ 127.0.0.1 │ 1 │ 2022-01-17T10:03:10+08:00 │ └───────────┴───────────┴──────────┴───────────────────────────┘ </code></pre> <h3>进阶操作 2:使用 Grafana 与 ClickHouse 对接</h3> <ol> <li>全局开启 <code>clickhouse-logger</code> 插件。</li> </ol> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/global_rules/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { "clickhouse-logger": { "timeout": 3, "retry_delay": 1, "batch_max_size": 100, "user": "default", "password": "a", "database": "default", "logtable": "t", "max_retry_count": 1, "endpoint_addr": "http://127.0.0.1:8123" } } }' </code></pre> <ol start="2"> <li>配置 <code>log_format</code> 元数据参数。<code>log_format</code> 的格式必须与数据库表的结构保持一致,否则会导致写入失败。</li> </ol> <pre><code class="language-shell"> curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/clickhouse-logger \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "log_format": { "upstream_header_time": "$upstream_header_time", "upstream_connect_time": "$upstream_connect_time", "status": "$status", "host": "$host", "body_bytes_sent": "$body_bytes_sent", "request": "$request", "remote_user": "$remote_user", "client_ip": "$remote_addr", "content_length": "$content_length", "local_time": "$fmt_ms_time_local", "http_referer": "$http_referer", "http_x_amz_target": "$http_x_amz_target", "http_x_request_id": "$http_x_request_id", "upstream_response_time": "$upstream_response_time", "upstream_status": "$upstream_status", "http_user_agent": "$http_user_agent", "request_time": "$request_time", "upstream_addr": "$upstream_addr", "http_host": "$http_host", "content_type": "$content_type" } }' </code></pre> <p>以下是使用 Grafana 与 Clickhouse 对接后的仪表盘视图。</p> <p><img src="https://static.apiseven.com/202108/1646366781343-ab2848fe-d10a-4222-a90d-79f4fe58999a.png" alt="Grafana-1" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/202108/1646366807867-4391a9ff-8b71-411c-8353-38957a5a2da1.png" alt="Grafana-2" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/202108/1646366832282-e8f24c63-c914-4051-8239-582bc3e58f50.png" alt="Grafana-3" referrerpolicy="no-referrer"></p> <h3>禁用 ClickHouse 插件</h3> <p>在插件配置中删除相应的配置即可禁用 <code>clickhouse-logger</code>。由于 Apache APISIX 插件是热加载模式,因此无需重新启动即可更新配置。</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "methods": ["GET"], "uri": "/hello", "plugins": {}, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } } }' </code></pre> <h2>总结</h2> <p>以上就是我为 Apache APISIX 开发 <code>clickhouse-logger</code> 的全过程。希望社区里有更多的人愿意走出舒适区,实现自身的角色转换,从关注者变为贡献者的过程远比你想象的简单。</p>

API 网关 Apache APISIX 携手 CoreDNS 打开服务发现新大门

<h2>背景信息</h2> <p>在传统的物理机和虚拟机部署中,各个服务之间的调用可以通过固定 <strong>IP + 端口</strong>的方式进行。随着云原生时代的到来,企业业务的部署更倾向于云原生容器化。但是在容器化环境中,服务实例的启动和销毁是非常频繁的,如果通过运维人员手动维护不仅工作量大,而且效果也欠佳。因此需要一种机制可以自动检测服务状态,当服务地址出现变更时,动态绑定新的地址。服务发现机制应运而生。</p> <h2>服务发现简介</h2> <p>服务发现机制可以分为两部分:</p> <ul> <li>服务注册中心:存储服务的主机和端口信息。</li> </ul> <p>假如某个容器对外提供计算平均值的服务,我们使用 <code>average</code> 的服务名作为唯一标识符,那么在服务注册中心就会以键值对(<code>average:192.168.1.21</code>)的方式存储。</p> <ul> <li>服务发现:允许其他用户发现服务注册阶段存储的信息。分为客户端发现模式和服务端发现模式。</li> </ul> <h3>客户端服务发现模式</h3> <p>在使用客户端发现模式时,客户端通过查询服务注册中心的存储信息,获取可用服务的实际网络地址后,通过负载均衡算法选择一个可用的服务实例,并将请求发送至该服务。</p> <ul> <li>优点:架构简单,扩展灵活,方便实现负载均衡功能。</li> <li>缺点:重客户端,强耦合,有一定开发成本。</li> </ul> <p><img src="https://static.apiseven.com/202108/1646299482001-9bba7b28-1780-44c8-869d-b75bc993c021.png" alt="error/client service discovery.png" referrerpolicy="no-referrer"></p> <p>客户端发现模式实现逻辑如下:</p> <ol> <li>新服务启动时,主动向注册中心注册,服务注册中心会存储新服务的服务名和地址;</li> <li>当客户端需要这个服务时,会使用服务名向服务注册中心发起查询;</li> <li>服务注册中心返回可用的地址,客户端再根据具体的算法选择其中一个发起调用。</li> </ol> <p>在这个过程中,除了服务注册,服务发现的工作基本由客户端独立完成,注册中心和服务端的地址对客户端也是完全可见的。</p> <h3>服务端服务发现模式</h3> <p>客户端向 Load Balancer 发送请求,Load Balancer 根据客户端的请求查询服务注册中心,找到可用的服务后转发请求到该服务上,和客户端服务发现模式一样,服务都需要在注册中心进行服务注册和注销。</p> <ul> <li>优点:服务的发现逻辑对客户端是透明的。</li> <li>缺点:需要额外部署和维护负载均衡器。</li> </ul> <p><img src="https://static.apiseven.com/202108/1646299531288-3ff99279-3ab6-49d7-8abf-68461f50c5c0.png" alt="error/server service discovery.png" referrerpolicy="no-referrer"></p> <p>服务端发现模式实现逻辑如下:</p> <ol> <li>新服务启动时,主动向注册中心注册,服务注册中心会存储新服务的服务名和地址;</li> <li>当客户端需要某个服务时,会使用服务名向负载均衡器发起查询;</li> <li>负载均衡器根据客户端请求的服务名,代理客户端向服务注册中心发起请求;</li> <li>负载均衡器获得返回的地址后,根据具体的算法选择其中一个发起调用。</li> </ol> <h2>使用 CoreDNS 的优势</h2> <p>CoreDNS 是一个用 <code>Go</code> 语言编写的开源 DNS 服务器,由于它的灵活性和可扩展性,常用于多容器环境中的 DNS 服务和服务发现。CoreDNS 建立在 Caddy 这个 HTTP/2 Web 服务器之上,实现了一个插件链的架构,将很多 DNS 相关的逻辑都抽象成了一层一层的插件,实现起来更灵活和易扩展,用户选择的插件会被编译到最终的可执行文件中,运行效率也非常高。CoreDNS 是首批加入 CNCF(云原生计算基金会)并且是已经毕业的云原生开源项目,也是 Kuberneters 中默认的 DNS 服务。</p> <p>相比于常见的服务发现框架(Apache ZooKeeper 和 Consul),CoreDNS 实现服务发现有哪些优势呢?</p> <p>服务发现的原理和计算机网络中重要的基础设施—— DNS 域名系统比较相似,DNS 域名系统把很少变动的域名与经常变动的服务器 IP 地址进行绑定,而服务发现机制则是把很少变动的服务名与服务地址绑定。由此我们可以借由 DNS 实现类似服务注册中心的功能,只需要将 DNS 中存储的域名转换为服务名即可。由于许多计算机内置了 DNS 功能,所以我们只需要在原有 DNS 系统上修改配置就可以了,不需要做太多额外的事情。</p> <h2>原理架构</h2> <p>整体架构如下:</p> <ol> <li>客户端向 APISIX 发起请求调用服务。</li> <li>APISIX 根据设置好的路由来访问上游服务节点(具体配置可见下文),在 APISIX 中可以设置上游信息通过 DNS 方式获取,只要正确设置了 DNS 服务器的 IP 地址,APISIX 会自动向该地址发起请求,获取 DNS 中对应服务的地址。</li> <li>CoreDNS 根据请求的服务名返回可用的地址列表。</li> <li>APISIX 根据可用地址和配置的算法,从其中选择一个发起调用。</li> </ol> <p><img src="https://static.apiseven.com/202108/1646299586044-3b44e6a8-b7a9-4ba6-a69c-8d08772b6065.png" alt="error/architecture.png" referrerpolicy="no-referrer"></p> <h2>如何使用</h2> <h3>前提条件</h3> <p>本文操作基于以下环境进行。</p> <ul> <li>操作系统 Centos 7.9。</li> <li>Apache APISIX 2.12.1,详情请参考:<a href="https://apisix.apache.org/zh/docs/apisix/how-to-build">如何构建 Apache APISIX</a>。</li> <li>CoreDNS 1.9.0,详情请参考:<a href="https://coredns.io/manual/toc/#installation">CoreDNS 安装指南</a>。</li> <li>Node.js 10.15.0,详情请参考:<a href="https://github.com/nodejs/help/wiki/Installation">Node.js 安装指南</a>。</li> </ul> <h3>操作步骤</h3> <ol> <li>使用 Node.js 的 <code>Koa</code> 框架在 <code>3005</code> 端口启动一个简单的测试服务。</li> </ol> <p>访问此服务会返回 <code>Hello World</code> 的字串,稍后我们将通过 CoreDNS 获取这个服务的地址。</p> <pre><code class="language-Shell"> // 使用 Koa 框架搭建服务 const Koa = require('koa'); const app = new Koa(); app.use(async ctx =&gt; { ctx.body = 'Hello World'; }); app.listen(3005); </code></pre> <ol start="2"> <li>配置 CoreDNS。</li> </ol> <p>CoreDNS 默认监听 <code>53</code> 端口,并且会读取在相同目录中的 <code>Corefile</code> 配置文件。初始条件下,同目录中并没有 <code>Corefile</code> 文件,因此我们需要创建并完成配置。</p> <p><code>Corefile</code> 主要是通过配置插件来实现功能,因此我们需要配置三个插件:</p> <ul> <li><code>hosts</code>:可以通过此参数将服务名和 IP 地址绑定,<code>fallthrough</code> 表示在当前插件无法返回正常数据时可以把请求转发给下一个插件处理(如果存在)。</li> <li><code>forward</code>:表示将请求代理到指定的地址,一般是权威 DNS 服务器地址。</li> <li><code>log</code>:不配置任何参数代表将日志信息打印到控制台界面,以便进行调试。</li> </ul> <pre><code class="language-Shell"> .:1053 { # 监听在1053端口 hosts { 10.10.10.10 hello # 将服务名 “coredns” 和IP地址绑定 fallthrough } forward . 114.114.114.114:53 log } </code></pre> <ol start="3"> <li>配置 Apache APISIX。</li> </ol> <p>在 <code>conf/config.yaml</code> 文件中添加相关配置并重新加载 Apache APISIX。</p> <pre><code class="language-Shell"> # config.yml # ...other config discovery: dns: servers: - "127.0.0.1:1053" # 使用 DNS 服务器的真实地址 # 此处为本机的1053端口 </code></pre> <ol start="4"> <li>配置 Apache APISIX 中的路由信息。</li> </ol> <p>接下来我们通过请求 <code>Admin API</code> 配置相关路由信息</p> <pre><code class="language-Shell"> curl http://127.0.0.1:9080/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "/core/*", "upstream": { "service_name": "hello:3005", # 将服务名命名为coredns,与CoreDNS中hosts插件的配置保持一致 "type": "roundrobin", "discovery_type": "dns" # 将服务发现类型设置为DNS } }' </code></pre> <ol start="5"> <li>验证。</li> </ol> <ul> <li>在本机上验证</li> </ul> <pre><code class="language-Shell"> curl 127.0.0.1:9080/core/hello -i HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 11 Connection: keep-alive Date: Wed, 16 Feb 2022 08:44:08 GMT Server: APISIX/2.12.1 Hello World </code></pre> <ul> <li>在其他主机上验证</li> </ul> <pre><code class="language-Shell"> curl 10.10.10.10:9080/core/hello -i HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 11 Connection: keep-alive Date: Wed, 16 Feb 2022 08:43:32 GMT Server: APISIX/2.12.0 Hello World </code></pre> <p>通过上述返回结果可以看到,服务是可以正常运行的。</p> <ol start="6"> <li>模拟容器因各种原因无法提供服务,导致 IP 地址发生变更。</li> </ol> <p>我们需要在另一台服务器上搭建相同的服务,同样运行在 <code>3005</code> 端口,但是 IP 地址发生变化,返回字符串改为 <code>Hello, Apache APISIX</code>。</p> <pre><code class="language-Shell">// 使用 Koa 框架搭建服务 const Koa = require('koa'); const app = new Koa(); app.use(async ctx =&gt; { ctx.body = 'Hello, Apache APISIX'; }); app.listen(3005); </code></pre> <p>修改 <code>Corefile</code> 配置并重启 CoreDNS,其他配置不作修改。配置示例如下:</p> <pre><code class="language-Shell">.:1053 { # 监听在1053端口 hosts { 10.10.10.11 hello # 修改服务IP地址 # 将服务名 “coredns” 和IP地址绑定 fallthrough } forward . 114.114.114.114:53 log } </code></pre> <blockquote> <p>DNS 存在缓存机制,当我们用 <code>dig</code> 命令请求解析新的域名时,在返回的 <code>DNS record</code> 中会看到一个数字字段,即 <code>TTL</code> 字段,一般是 <code>3600</code>,即一个小时。在 <code>TTL</code> 时间段内发往该域名的请求不会再向 DNS 服务器请求解析地址,而是直接在本地缓存中获取该域名对应的地址。</p> </blockquote> <p>通过验证我们可以发现,请求已经重新指向新的地址。验证如下:</p> <pre><code class="language-Shell"> curl 127.0.0.1:9080/core/hello -i HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 11 Connection: keep-alive Date: Wed, 16 Feb 2022 08:44:08 GMT Server: APISIX/2.12.0 Hello, Apache APISIX </code></pre> <h2>总结</h2> <p>本文主要介绍了服务发现的类型以及在 Apache APISIX 中如何使用 CoreDNS。您可以根据自身的业务需求和过往技术架构使用 Apache APISIX 与 CoreDNS。</p> <p>Apache APISIX 项目目前正在开发其他插件以支持集成更多服务,如果您对此有兴趣,您可以通过 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 发起讨论,或通过<a href="https://apisix.apache.org/docs/general/subscribe-guide">邮件列表</a>进行交流。</p>

开源云 IDE 产品新宠儿,如何使用 Gitpod 开发 API 网关 Apache APISIX?

<p>随着云原生浪潮的来临,软件开发流程中的各个环节都在发生变革,其中有一个非常热门的方向就是“云 IDE”。所谓“云 IDE”就是使用云端的计算资源作为开发环境,进行软件项目的开发。</p> <p>这种开发模式对于开发者来说有很多好处,例如:</p> <ul> <li>计算资源予取予求,不会因为硬件限制影响开发效率;</li> <li>开发环境标准化,每个项目的开发环境可能有很多软件依赖,这些依赖可以通过 Docker 镜像的形式进行标准化;</li> <li>快速为每个项目启动或摧毁一个开发环境,避免多个项目并行开发时存在依赖冲突等问题;</li> <li>纯粹的 Linux 环境。对于服务端开发同学来说,MacOS 和 Windows 的依赖问题往往比开发项目代码还要困难;</li> </ul> <p>目前最流行的两种 IDE,非 JetBrains 系和 VSCode 莫属,而这两种广受欢迎的开发工具都有相关的云产品面世,可见很多开发者看好“云 IDE”这个方向。</p> <p>Apache APISIX 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。</p> <p>Apache APISIX 作为开源的云原生 API 网关,如何快速的部署 APISIX 的开发环境对于开发者是比较重要的。本文将为大家介绍如何使用 <a href="https://gitpod.io/">Gitpod</a> 对 Apache APISIX 进行相关开发。</p> <h2>安装 Gitpod Chrome 插件</h2> <p>Gitpod 为三个主流代码托管服务 GitLab、GitHub 和 Bitbucket 提供了一键启动功能,您只需要安装 <a href="https://chrome.google.com/webstore/detail/gitpod-always-ready-to-co/dodmmooeoklaejobgleioelladacbeki">Chrome 插件</a>即可进行后续使用。</p> <p>安装完成后,这个插件会在代码仓库页面注入一个启动按钮,以 GitHub 为例。安装插件后打开 APISIX 项目地址,可以看到相关按钮:</p> <p><img src="https://static.apiseven.com/202108/1646233179407-391328ba-68cd-41df-8454-3c7d280bbc6e.png" alt="error/github example.png" referrerpolicy="no-referrer"></p> <p>单击 Gitpod 按钮会跳转到 Gitpod 的页面,完成 GitHub 应用授权后,进入以下界面:</p> <p><img src="https://static.apiseven.com/202108/1646233426671-547eb71c-9294-43af-b144-ea3298343341.png" alt="erro/gitpod UI.png" referrerpolicy="no-referrer"></p> <p>是不是非常熟悉? 对,这就是时下最流行的代码编辑器 VSCode。</p> <p>为了实现 VSCode 客户端与服务端分离的架构,Gitpod 维护一个 VSCode 的<a href="https://github.com/gitpod-io/openvscode-server">分支</a>。作为一个云上的 VSCode ,它与桌面版的功能一致。我们在本地开发时常用的插件一样可以在云上使用,与本地不同的是,云上的 VSCode 有着服务器级别的计算资源和网络环境。</p> <h2>使用 Gitpod 搭建 APISIX 开发环境</h2> <h3>步骤一:执行测试用例</h3> <p>相信很多刚接触开源的同学,都为如何搭建开源项目的开发环境感到过困扰,因为开源项目往往都会存在大量自动化执行的测试用例来保证整个项目的质量,所以如何在开发者在本地运行这些测试用例,可能是我们遇到的第一个问题,这一点和企业内的开发可能有比较大的差异。</p> <p>下面我们尝试在 Gitpod 中运行 APISIX 的测试用例,这里可以参考 APISIX 仓库中的 <a href="https://github.com/apache/apisix/blob/master/.github/workflows/build.yml">github workflow</a> 配置依赖。在 Gitpod 的终端中执行如下步骤:</p> <pre><code class="language-Shell"># 启动CI依赖的组件 make ci-env-up project_compose_ci=ci/pod/docker-compose.common.yml # 安装编译依赖 sudo apt install -y cpanminus build-essential libncurses5-dev libreadline-dev libssl-dev perl libpcre3 libpcre3-dev libldap2-dev # 进行编译和执行测试用例 sudo OPENRESTY_VERSION=default ./ci/linux_openresty_1_17_runner.sh do_install sudo ./ci/linux_openresty_1_17_runner.sh script </code></pre> <p>整个过程会非常流畅,因为这里有一个隐藏的优势:Gitpod 运行环境是在国外的,所以下载各种依赖的速度会非常的快,不会遇到网络问题。</p> <h3>步骤二:访问 HTTP 服务</h3> <p>那么对于项目中启动的 HTTP 服务(例如 APISIX)我们要如何访问呢?</p> <p>通过终端访问自然是可以的,但如果你想要通过页面访问,也可以通过 Remote Explorer 将打开的端口暴露到公网上, 如下图所示:</p> <p><img src="https://static.apiseven.com/202108/1646234288822-b7e30fce-604f-451a-b87f-3b72309b246a.png" alt="error/access page example.png" referrerpolicy="no-referrer"></p> <p>然后再通过单击端口右侧的浏览器图标,Gitpod 会自动打开一个链接,就可以访问到这个端口对应的服务了。</p> <h2>常见问题汇总</h2> <h3>浏览器端体验</h3> <p>在浏览器中使用 Gitpod 有一个很大的问题是,很多 VSCode 的快捷键会被浏览器捕获,导致无法执行对应的操作,这时我们就可以使用 VSCode 的 Gitpod 插件来实现更加原生的编码体验。</p> <p>具体方法如下:</p> <ol> <li>在 VSCode 的插件市场,安装 Gitpod 插件。</li> </ol> <p><img src="https://static.apiseven.com/202108/1646234524665-0e860b0b-ec80-4ba9-a893-cfa79d3f48c3.png" alt="error/install gitpod plugin.png" referrerpolicy="no-referrer"></p> <ol start="2"> <li>在浏览器的 Gitpod 页面单击 <code>Gitpod: Open in VS Code</code> 就可以实现拉起本地的 VSCode 作为客户端连接到云端的 Gitpod ,达到与桌面版相同效果的编码体验。</li> </ol> <p><img src="https://static.apiseven.com/202108/1646234630208-bc8912a8-9542-4888-9cde-8889631d2ea8.png" alt="error/open in vs code.png" referrerpolicy="no-referrer"></p> <h3>私有化部署</h3> <p>前面我们提到 Gitpod 是一款开源产品,所以是完全可以在组织内部进行私有化部署服务的,这样就可以在私有代码仓库中使用上这款优秀的开发工具。具体部署方式可以参考 Gitpod 的<a href="https://github.com/gitpod-io/gitpod">官方文档和仓库</a>。</p> <h2>总结</h2> <p>Gitpod 的优点是可以让开发者快速上手一个项目,这一点非常符合开源社区的需求。因为开源项目往往会给刚刚接触开源的开发者一种神秘莫测的感觉,让人望而却步,但真正接触后你会发现并不如此。</p> <p>希望通过本文的介绍和描述,可以让每一个对开源项目感兴趣的开发者借助开源工具加持下,都能更加轻松得加入到开源社区中去,让开源的生态持续繁荣。</p>

GraphQL 碰撞 API 网关 Apache APISIX,提升 API 领域的安全与性能

<h2>背景信息</h2> <p>GraphQL 是一个开源的、面向 API 而创造出来的数据查询操作语言以及相应的运行环境。最初由 Facebook 于 2012 年内部开发,2015 年公开发布。2018 年 11 月 7 日,Facebook 将 GraphQL 项目转移到新成立的 GraphQL 基金会。</p> <p>您可以把 GraphQL 类比为 SQL 查询语句来理解,与 SQL 查询语句相比,GraphQL 对 API 中的数据提供了一套易于理解的完整描述,让客户端能够通过自定义的描述来准确获得其所需要的数据。这也让 API 能够从容面对日益复杂的接口发展,并避免最终成为一个令人望而生畏的复杂接口。</p> <p>Apache APISIX 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。</p> <p>Apache APISIX 作为云原生 API 网关,在设计之初就已经具备识别 GraphQL 语法的匹配能力。通过对请求中携带的 GraphQL 语句进行高效匹配,筛除异常流量,进一步保证安全性和提高系统性能。</p> <h3>场景解析</h3> <p>我们正处于大数据大流量的时代,Apache APISIX 和 GraphQL 可以通过结合的方式,形成共赢的局面。接下来举一个场景具体说明。</p> <p>本文将会讨论微服务架构场景下的 Apache APISIX 和 GraphQL 的实际应用。</p> <h3>实际场景中遇到的问题</h3> <p>在项目进行到后期时,往往会出现业务复杂化、团队人员流动性高等问题,而微服务架构已经成为解决这类问题的常见解决方案。在微服务架构中,GraphQL 暴露出的接口分为分散式和集中式两种,然而只有集中式接口设计才能够最大化体现 GraphQL 的优势,但是在集中式接口设计中,所有的微服务对外暴露的是同一个接口,<strong>因此处理流量的路由就不能简单地根据 URL 进行转发,而是应该根据请求中包含的不同字段进行转发</strong>。</p> <p>由于 NGINX 处理请求时仅会处理 URL 以及一些参数,但是只有解析请求参数中的查询信息才可以知道客户端访问的资源,从而进行路由转发,因此这种路由转发方式通过传统的 NGINX 是无法完成的。在实际应用场景中,将 GraphQL 接口直接对外暴露非常危险,因此需要一个专业的高性能 API 网关保护 GraphQL 的接口。</p> <h3>解决方案</h3> <p>基于 Apache APISIX 安全、稳定、高性能的特性,增加 GraphQL 灵活的路由匹配规则是解决 GraphQL 集中式接口设计问题的最佳方案。</p> <p><img src="https://static.apiseven.com/202108/1646200966179-1d649ab0-8d49-49f5-a8fa-a1a30af0519d.png" alt="error/graphql architecture.png" referrerpolicy="no-referrer"></p> <p>在此方案中,Apache APISIX 作为 API 网关部署在 GraphQL Server 之前,为整个后端系统提供了安全保障,并且 Apache APISIX 根据自身所具备的 GraphQL 匹配功能,筛选一部分请求之后再由 GraphQL Server 进行处理,使整个请求资源过程变得更高效。</p> <p>得益于 Apache APISIX 的动态特性,您可以启用限流限速、身份认证、可观测性等插件,无需重启服务,进一步提高此方案的运行效率,方便运维工作。</p> <p>此外,Apache APISIX 还可以针对不同的 <code>graphql_operation</code> 进行不同的权限校验、针对不同的 <code>graphql_name</code> 转发到不同的 Upstream,具体细节将会在下文中进行描述。</p> <p><strong>综上所述,Apache APISIX + GraphQL 的解决方案,在充分利用 GraphQL 搜索优势的同时也能拥有 Apache APISIX 作为 API 网关所具备的安全性和稳定性。</strong></p> <h2>GraphQL 在 Apache APISIX 中的应用</h2> <h3>基本逻辑</h3> <p><img src="https://static.apiseven.com/202108/1646201215532-f5965158-7456-443a-84a7-cadadb95fc1f.png" alt="error/GraphQL principle.png" referrerpolicy="no-referrer"></p> <p>目前 GraphQL 在 Apache APISIX 中的执行逻辑如下:</p> <ol> <li>Clients 向 Apache APISIX 发起带有 GraphQL 语句的请求;</li> <li>Apache APISIX 匹配路由并提取预设的 GraphQL 数据;</li> <li>Apache APISIX 通过预设 GraphQL 数据对请求数据进行匹配;</li> </ol> <ul> <li>匹配成功,Apache APISIX 将继续转发请求;</li> <li>匹配失败,Apache APISIX 将立刻终止请求。</li> </ul> <ol start="4"> <li>是否存在插件;</li> </ol> <ul> <li>如果存在插件,请求将继续被插件处理,处理完成后,继续转发到 GraphQL Server;</li> <li>如果不存在插件,请求将直接转发到 GraphQL Server。</li> </ul> <p>在 APISIX core 里内部匹配中,Apache APISIX 通过 <a href="https://github.com/bjornbytes/graphql-lua"><code>graphql-lua</code></a> 库实现对 GraphQL 的支持。Apache APISIX GraphQL 解析库会先对携带 GraphQL 语法的请求进行解析,之后将解析后的请求与预设在 Apache APISIX 数据库里的配置数据进行匹配。如果匹配成功 Apache APISIX 会放行并转发请求,反之则终止请求</p> <h3>具体配置</h3> <p>Apache APISIX 目前支持通过 GraphQL 的一些属性过滤路由:</p> <ul> <li>graphql_operation</li> <li>graphql_name</li> <li>graphql_root_fields</li> </ul> <p>GraphQL 的属性与下面的所展示 GraphQL query 语句一一对应:</p> <pre><code class="language-Nginx"> query getRepo { owner { name } repo { created } } </code></pre> <ul> <li><code>graphql_operation</code> 对应 <code>query</code></li> <li><code>graphql_name</code> 对应 <code>getRepo</code></li> <li><code>graphql_root_fields</code> 对应 <code>["owner", "repo"]</code></li> </ul> <p>您可以通过如下示例为 Apache APISIX 设置一条路由来验证对 GraphQL 的匹配能力:</p> <pre><code class="language-Shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "methods": ["POST"], "uri": "/_graphql", "vars": [ ["graphql_operation", "==", "query"], ["graphql_name", "==", "getRepo"], ["graphql_root_fields", "has", "owner"] ], "upstream": { "type": "roundrobin", "nodes": { "192.168.1.200:4000": 1 } } }' </code></pre> <p>接下来使用带有 GraphQL 语句的请求去访问:</p> <pre><code class="language-Shell">curl -H 'content-type: application/graphql' \ -X POST http://127.0.0.1:9080/graphql -d ' query getRepo { owner { name } repo { created } }' </code></pre> <p>如果匹配成功,则 Apache APISIX 继续进行请求转发。</p> <pre><code class="language-Shell">HTTP/1.1 200 OK </code></pre> <p>反之,则终止请求。</p> <pre><code class="language-Shell">HTTP/1.1 404 Not Found </code></pre> <h2>进阶操作</h2> <p>Apache APISIX 可以根据不同的 <code>graphql_name</code> 转发到不同的 Upstream,以及根据不同 <code>graphql_operation</code> 进行不同的权限校验。下文将为您展示此特性的代码配置。</p> <h3>使用 <code>graphql_name</code> 匹配 Upstream</h3> <ol> <li>创建第一个上游服务:</li> </ol> <pre><code class="language-Shell"> curl http://192.168.1.200:9080/apisix/admin/upstreams/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "type": "chash", "key": "remote_addr", "nodes": { "192.168.1.200:1980": 1 } }' </code></pre> <ol start="2"> <li>创建与第一个上游服务绑定的 GraphQL 路由,<code>graphql_name</code> 设置为 <code>getRepo111</code>:</li> </ol> <pre><code class="language-Shell"> curl http://192.168.1.200:9080/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "methods": ["POST"], "uri": "/graphql", "vars": [ ["graphql_operation", "==", "query"], ["graphql_name", "==", "getRepo111"], ["graphql_root_fields", "has", "owner"] ], "upstream_id": "1" }' </code></pre> <ol start="3"> <li>创建第二个上游服务:</li> </ol> <pre><code class="language-Shell"> curl http://192.168.1.200:9080/apisix/admin/upstreams/2 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "type": "chash", "key": "remote_addr", "nodes": { "192.168.1.200:1981": 1 } }' </code></pre> <ol start="4"> <li>创建与第二个上游服务绑定的 GraphQL 路由,<code>graphql_name</code> 设置为 <code>getRepo222</code>:</li> </ol> <pre><code class="language-Shell"> curl http://192.168.1.200:9080/apisix/admin/routes/2 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "methods": ["POST"], "uri": "/graphql", "vars": [ ["graphql_operation", "==", "query"], ["graphql_name", "==", "getRepo222"], ["graphql_root_fields", "has", "owner"] ], "upstream_id": 2 }' </code></pre> <ol start="5"> <li>测试示例</li> </ol> <p>使用之前所创建的两个 <code>graphql_name</code> 服务进行测试,您可以发现 Apache APISIX 可以根据请求中不同的 <code>graphql_name</code> 自动选择转发的 Upstream。</p> <ul> <li>如果请求是此示例:</li> </ul> <pre><code class="language-Shell"> curl -i -H 'content-type: application/graphql' \ -X POST http://192.168.1.200:9080/graphql -d ' query getRepo111 { owner { name } repo { created } }' </code></pre> <p>则会返回来自上游 <code>192.168.1.200:1980</code> 的响应:</p> <pre><code class="language-Shell"> HTTP/1.1 200 OK ---URI /graphql ---Service Node Centos-port: 1980 </code></pre> <ul> <li>如果请求是这个示例:</li> </ul> <pre><code class="language-Shell"> curl -i -H 'content-type: application/graphql' \ -X POST http://192.168.1.200:9080/graphql -d ' query getRepo222 { owner { name } repo { created } }' </code></pre> <p>则会返回来自上游 <code>192.168.1.200:1981</code> 的响应:</p> <pre><code class="language-Shell"> HTTP/1.1 200 OK ---URI /graphql ---Service Node Centos-port: 1981 </code></pre> <h3>使用 <code>graphql_operation</code> 进行不同的权限校验</h3> <p>上述示例提供了 <code>graphql_operation</code> 为 <code>query</code> 的匹配规则,现在使用 <code>mutation</code> 形式的 GraphQL 请求。</p> <ol> <li>配置 Apache APISIX:</li> </ol> <pre><code class="language-Shell">curl http://192.168.1.200:9080/apisix/admin/routes/11 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "methods": ["POST"], "uri": "/hello", "vars": [ ["graphql_operation", "==", "mutation"], ["graphql_name", "==", "repo"] ], "upstream": { "nodes": { "192.168.1.200:1982": 1 }, "type": "roundrobin" } }' </code></pre> <ol start="2"> <li>发起 <code>mutation</code> 请求用来验证 Apache APISIX 的配置:</li> </ol> <pre><code class="language-Shell">curl -i -H 'content-type: application/graphql' \ -X POST http://192.168.1.200:9080/hello -d ' mutation repo($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { stars commentary } }' </code></pre> <p>返回结果如下所示:</p> <pre><code class="language-Shell">HTTP/1.1 200 OK ---URI /hello ---Service Node Centos-port: 1982 </code></pre> <h2>搭配插件</h2> <p>Apache APISIX 拥有丰富的插件生态以应对不同的使用场景,如果在使用 Apache APISIX + GraphQL 时添加适合的插件,就可以使方案应用的场景变得更多。</p> <p>本文仅选取了以下两种类型的插件进行举例。</p> <h3><code>limit-count</code> 速度插件</h3> <p>搭配使用 <code>limit-count</code> 插件,流量经过 GraphQL 匹配规则得到转发之后进一步被限制。得益于 Apache APISIX 的特性,可以达到动态、精细化、分布式的限流限速。详情请参考 <a href="https://apisix.apache.org/zh/docs/apisix/plugins/limit-count">Apache APISIX 官方文档</a>。</p> <h3>可观测性插件</h3> <p>Apache APISIX 提供了包括但不仅限于 <a href="https://apisix.apache.org/zh/docs/apisix/plugins/prometheus"><code>prometheus</code></a>、<a href="https://apisix.apache.org/zh/docs/apisix/plugins/skywalking"><code>skywalking</code></a> 等可观测性插件,能够为系统提供更多监控指标数据,方便系统后续的运营维护等实现。</p> <h2>总结</h2> <p>本文为大家初步介绍了 GraphQL 在 Apache APISIX 中的应用,并且使用实际代码,为您展示了Apache APISIX 与 GraphQL 两项技术的结合,用户可以根据自身的业务需求和实际场景在 Apache APISIX 中使用 GraphQL。</p> <p>关于 GraphQL 的更多说明和完整配置信息,可参考 <a href="https://apisix.apache.org/zh/docs/apisix/router-radixtree/#%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87-graphql-%E5%B1%9E%E6%80%A7%E8%BF%87%E6%BB%A4%E8%B7%AF%E7%94%B1">Apache APISIX官方文档</a>。</p> <p>Apache APISIX 项目目前正在开发其他插件以支持集成更多服务,如果您对此有兴趣,您可以通过 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 发起讨论,或通过<a href="https://apisix.apache.org/docs/general/subscribe-guide">邮件列表</a>进行交流.</p>

新插件上线,API 网关 Apache APISIX Public API 处理能力再增强

<h2>背景信息</h2> <p>Apache APISIX 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。作为 API 网关,Apache APISIX 不仅拥有众多实用的插件,而且支持插件动态变更、热插拔和开发自定义插件。</p> <p>当前用户在 Apache APISIX 中开发自定义插件时,可以为插件定义一些 API(下称 public API),比如在当前的 <code>jwt-auth</code> 插件中,它实现并提供了一个 <code>/apisix/plugin/jwt/sign</code> 接口用于签发 JWT,由于此接口不是通过 Admin API 添加的,因此无法像管理路由一样管理此类接口。</p> <p>在实际应用场景中,提供的接口是面向内部调用的,而非开放在公网供任何人调用。为了应对这种场景,Apache APISIX 设计了 <code>public-api</code> 插件。通过这个插件,可以解决 public API 使用过程中的痛点,您可以为 public API 设置自定义的 <code>uri</code>,可以配置任何类型的插件。</p> <h2>初识 public-api</h2> <p>本节以 <code>jwt-auth</code> 插件的 <code>/apisix/plugin/jwt/sign</code> 接口为例,为您介绍 <code>public-api</code> 插件两种使用方法和一种场景示例。</p> <p>在使用 <code>public-api</code> 插件之前,如果在插件开发中使用 <code>_M.api()</code> 注册了 public API 后,APISIX 会默认将它暴露出来,您可以直接在 HTTP 端口调用这个 API。现在,您需要手动创建一个路由,配置 <code>public-api</code> 插件,才可以将 API 转发至 <code>public-api</code> 插件中。</p> <h3>确认 API 是否被开放</h3> <p>您可以通过下述命令请求 API 地址,通过返回结果可以看到 <code>/apisix/plugin/jwt/sign</code> 默认情况下并没有被暴露出来,是不可用的。</p> <pre><code class="language-Shell">curl -XGET 'http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key' {"error_msg":"404 Route Not Found"} </code></pre> <h3>前提条件</h3> <p>您需要创建 Consumer 并开启 <code>jwt-auth</code> 插件,才可以执行以下步骤。</p> <blockquote> <p>示例中 <code>jwt-auth</code> 参数配置信息,请参考 <a href="https://apisix.apache.org/zh/docs/apisix/plugins/jwt-auth">Apache APISIX 官方文档</a>。</p> </blockquote> <pre><code class="language-Shell">curl -XPUT 'http://127.0.0.1:9080/apisix/admin/consumers' \ -H 'X-API-KEY: &lt;api-key&gt;' \ -H 'Content-Type: application/json' \ -d '{ "username": "APISIX", "plugins": { "jwt-auth": { "key": "user-key", "algorithm": "HS256" } } }' </code></pre> <h3>方法一:基础使用</h3> <ol> <li>设置路由</li> </ol> <p>根据<strong>前提条件</strong>中的 Consumer 创建 Route,设置 <code>uri</code> 为 <code>jwt-auth</code> 插件中签发 <code>JWT</code> 的 API 地址,并在此 Route 中开启 <code>public-api</code> 插件。</p> <pre><code class="language-Shell"> curl -XPUT 'http://127.0.0.1:9080/apisix/admin/routes/r1' \ -H 'X-API-KEY: &lt;api-key&gt;' \ -H 'Content-Type: application/json' \ -d'{ "uri": "/apisix/plugin/jwt/sign", "plugins": { "public-api": {} } }' </code></pre> <ol start="2"> <li>测试示例</li> </ol> <p>使用如下命令进行测试,如果您看到返回结果是一个 JWT 字符串,表示此 public API 已经可以使用。</p> <pre><code class="language-Shell"> curl -XGET 'http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key' &lt;header&gt;.&lt;payload&gt;.&lt;signature&gt; </code></pre> <h3>方法二:自定义路径</h3> <p>在使用 <code>public-api</code> 插件之前,用户想要修改一个 public API 对外开放的 <code>uri</code>,是比较困难的。使用 <code>prometheus</code> 插件的用户可以通过修改配置文件的方法自定义 <code>exporter uri</code>,但是对于其他 Apache APISIX 的插件,只能通过修改插件文件的方式来实现,而在生产环境中此操作是有困难且有风险的。</p> <p>现在您可以使用 <code>public-api</code> 插件修改 public API 对外开放的 <code>uri</code>,具体操作示例如下。</p> <ol> <li>设置路由</li> </ol> <p>使用如下命令修改<strong>方法一</strong>中创建的 Route,并设置 <code>uri=/gen_token</code>,同时将原有的 <code>uri</code> 配置到 <code>public-api</code> 插件中的 <code>uri</code> 字段。</p> <pre><code class="language-Shell"> curl -XPUT 'http://127.0.0.1:9080/apisix/admin/routes/r1' \ -H 'X-API-KEY: &lt;api-key&gt;' \ -H 'Content-Type: application/json' \ -d '{ "uri": "/gen_token", "plugins": { "public-api": { "uri": "/apisix/plugin/jwt/sign" } } }' </code></pre> <ol start="2"> <li>测试示例</li> </ol> <p>使用新 <code>uri</code> 可以正常访问 public API。</p> <pre><code class="language-Shell"> curl -XGET 'http://127.0.0.1:9080/gen_token?key=user-key' &lt;header&gt;.&lt;payload&gt;.&lt;signature&gt; </code></pre> <p>使用旧 <code>uri</code> 无法访问 public API。</p> <pre><code class="language-Shell"> curl -XGET 'http://127.0.0.1:9080/apisix/plugin/jwt/sign?key=user-key' {"error_msg":"404 Route Not Found"} </code></pre> <h3>场景示例:保护路由</h3> <p>本节将介绍如何使用 <code>public-api</code> 插件解决 <code>plugin-interceptors</code> 插件所带来的业务痛点。</p> <p>以下步骤以 <code>key-auth</code> 插件为例,为您介绍如何使用 <code>public-api</code> 插件保护 public API。</p> <blockquote> <p>示例中 <code>key-auth</code> 配置信息,请参考 <a href="https://apisix.apache.org/zh/docs/apisix/plugins/jwt-auth">Apache APISIX 官方文档</a>。</p> </blockquote> <ol> <li>创建 Consumer</li> </ol> <p>创建 Consumer,并配置 <code>key-auth</code> 密钥。</p> <pre><code class="language-Shell"> curl -XPUT 'http://127.0.0.1:9080/apisix/admin/consumers' \ -H 'X-API-KEY: &lt;api-key&gt;' \ -H 'Content-Type: application/json' \ -d '{ "username": "APISIX", "plugins": { "key-auth": { "key": "test-apikey" } } }' </code></pre> <ol start="2"> <li>设置路由</li> </ol> <p>修改<strong>方法二</strong>中创建的路由,并开启 <code>key-auth</code> 插件和 <code>public-api</code> 插件。</p> <pre><code class="language-Shell"> curl -XPUT 'http://127.0.0.1:9080/apisix/admin/routes/r1' \ -H 'X-API-KEY: &lt;api-key&gt;' \ -H 'Content-Type: application/json' \ -d '{ "uri": "/gen_token", "plugins": { "public-api": { "uri": "/apisix/plugin/jwt/sign" }, "key-auth": {} } }' </code></pre> <ol start="3"> <li>测试示例</li> </ol> <p>经过测试,当请求携带正确的 <code>apikey</code> 时,public API 可以正常响应,而没有携带 <code>apikey</code> 时,将返回 <code>401</code> 未认证的状态码。如果您测试的返回结果和示例状态一致,则证明您刚刚配置的 <code>key-auth</code> 插件已经生效。</p> <pre><code class="language-Shell"> # with corrent apikey curl -XGET 'http://127.0.0.1:9080/gen_token?key=user-key' -H "apikey: test-apikey" &lt;header&gt;.&lt;payload&gt;.&lt;signature&gt; # without apikey curl -i -XGET 'http://127.0.0.1:9080/gen_token?key=user-key' HTTP/1.1 401 UNAUTHORIZED </code></pre> <h2>原理详解</h2> <p>从上述示例中您可以看出,<code>public-api</code> 插件可以很好的解决用户在使用 public API 时的缺陷。本节为您详细介绍实现原理。</p> <p>关于 <code>public-api</code> 的原理,可以使用一句话描述:<code>public-api</code> 插件将之前单独的 public API 路由匹配转移到插件内部,仅对开启插件的路由进行 public API 匹配。以下将从两个方面为您详细解释原理。</p> <h3>使用 <code>public-api</code> 插件之前</h3> <p>首先,您需要了解 Apache APISIX 在集成 <code>public-api</code> 插件之前是如何实现 public API 的功能的。</p> <ul> <li>当 APISIX 启动时会加载自定义插件,并使用从 etcd 获取的 Route 配置构建 radixtree 路由器,它将负责根据请求信息匹配 Route 并调用正确的 <code>handler</code> 来转发请求。</li> <li>APISIX 将为自定义插件的 public API 与用户创建的 Route 分别创建不同的路由器(下文分别称为 public API 路由器和 Route 路由器)</li> <li>当请求到达时,将先由 public API 路由器进行匹配,之后再由 Route 路由器进行匹配。它们在请求处理流程上是完全分开的两个部分。</li> </ul> <p><img src="https://static.apiseven.com/202108/1646120195055-fff81b45-55bb-4100-8822-b14b173448d5.png" alt="error/flowchart.png" referrerpolicy="no-referrer"></p> <p>根据此流程,如果您想将面向 Route 路由器的插件应用在 public API 路由器上,就需要手动维护一个插件列表,并在 public API 路由器匹配到之后手动执行插件函数。由此可以看出,这样的架构是复杂且难以维护的,并且带来了许多问题,如使用复杂(基于 <code>plugin_metadata</code> 的配置方式)、粗粒度配置(难以为一个插件中提供的多个 public API 执行不同的策略)等。</p> <h3>增加 <code>public-api</code> 插件之后</h3> <p>在我们引入了 <code>public-api</code> 插件后,上述流程将会被简化,将原来先于 Route 路由匹配执行的 public API 路由匹配被转移到了插件中。</p> <ul> <li>当请求到达时,APISIX 会直接执行 Route 路由匹配,当找到相应的路由后,将转发请求至插件中进行处理。</li> <li>当一个 Route 开启了 <code>public-api</code> 插件时,将根据插件的配置调用指定的 public API 进行请求处理,不再执行请求的转发。而没有开启 <code>public-api</code> 插件的 Route,将不会进行处理。</li> </ul> <p><img src="https://static.apiseven.com/202108/1646136319962-68f66607-804c-4cbc-8742-0745a3ad0f5a.png" alt="error/flowchart.png" referrerpolicy="no-referrer"></p> <p>自定义插件提供的 public API 默认将不再暴露出来,而是由用户配置 Route 来决定以何种方式提供,可以自由的设置路由参数,如 <code>uri</code>、<code>host</code>、<code>method</code> 等,之后只需要为路由开启 <code>public-api</code> 插件即可。</p> <p>由于 <code>public-api</code> 插件具有较低的优先级,它将在大部分插件执行完之后再执行,这样用户就可以为 Route 配置任意认证和安全类插件。</p> <p>Apache APISIX 不再进行两阶段的 Route 路由匹配和执行不同的逻辑,一切归于 Route 路由匹配,请求处理的流程也被简化。</p> <h2>总结</h2> <p>需要注意,<code>public-api</code> 在被纳入正式版本发布之后,在 APISIX 的 HTTP 请求处理流程中,Apache APISIX 将不再进行 public API 的路由匹配,即默认不暴露插件中注册的 public API。您可以参考上述 <code>public-api</code> 插件的操作示例更加灵活的使用 public API 的功能。</p> <blockquote> <p>此插件已经在 <code>APISIX v2.13.0</code> 版本上线支持,如果您已经在 <code>APISIX v2.13.0</code> 之前的版本完成自定义插件的开发,升级版本后会对您的业务造成影响,请您升级前再次确认。</p> </blockquote> <p>关于 <code>public-api</code> 插件的更多说明和完整配置信息,您可以参考 <a href="https://apisix.apache.org/docs/apisix/next/plugins/public-api">Apache APISIX 官方文档</a>。</p> <p>Apache APISIX 项目目前正在开发其他插件以支持集成更多服务,如果您对此有兴趣,您可以通过 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 发起讨论,或通过<a href="https://apisix.apache.org/docs/general/subscribe-guide">邮件列表</a>进行交流.</p>

可观测性能力升级,API 网关 Apache APISIX 集成 OpenTelemetry

<h2>背景信息</h2> <p>Apache APISIX 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。作为 API 网关,Apache APISIX 不仅拥有众多实用的插件,而且支持插件动态变更和热插拔。</p> <p>OpenTelemetry 是一个开源的遥测数据采集和处理系统,它不仅提供了各种 SDK 用于应用端遥测数据的收集和上报,以及数据收集端用于数据接收、加工和导出,还支持通过配置导出到任意一个或者多个已经适配 OpenTelemetry Exporter 的后端,比如 Jaeger、Zipkin、OpenCensus 等。您可以在 opentelemetry collector contrib 库中查看已经适配 OpenTelemetry Collector 的插件列表。</p> <p><img src="https://static.apiseven.com/202108/1646037628714-f542841e-ac27-4c13-a4c8-4cdef79ee501.png" alt="error/OpenTelemetry" referrerpolicy="no-referrer"></p> <h2>插件介绍</h2> <p>Apache APISIX <code>opentelemetry</code> 插件是基于 OpenTelemetry 原生标准(<a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#otlphttp-request">OTLP/HTTP</a>)实现的 Tracing 数据采集,并通过 HTTP 协议发送至 OpenTelemetry Collector。该功能将在 Apache APISIX 2.13.0 版本中上线支持。</p> <p>由于 OpenTelemetry 的 Agent/SDK 与后端实现无关,当应用集成了 OpenTelemetry 的 Agent/SDK 之后,用户能够在应用侧无感知的情况下轻松地、自由地变更可观测性后端服务,比如从 Zipkin 切换成 Jaeger。</p> <p><code>opentelemetry</code> 插件在 Apache APISIX 中集成了 OpenTelemetry Agent/SDK,可以实现采集被追踪的请求生成 trace 后转发到 OpenTelemetry Collector。</p> <p><code>opentelemetry</code> 插件位于上图中的 Agent 侧,但目前仅支持 <code>trace</code> 协议,还不支持 OpenTelemetry 的 <code>logs</code> 和 <code>metrics</code> 协议。</p> <h2>如何使用</h2> <h3>启用插件</h3> <p>您需要在 <code>conf/config.yaml</code> 配置文件中启用 <code>opentelemetry</code> 插件并修改 <code>collector</code> 配置。</p> <p>假设您已经完成 OpenTelemetry Collector 的部署,并且启用了 <a href="https://github.com/open-telemetry/opentelemetry-collector/blob/main/receiver/otlpreceiver/README.md">OTLP HTTP Receiver</a>。</p> <blockquote> <p>如果您未完成部署,可参考下一节的场景示例部分,完成 OpenTelemetry Collector 的部署。</p> </blockquote> <p>其中 OTLP HTTP Receiver 的默认端口为<code>4318</code>,<code>collector</code> 的地址为 OpenTelemetry Collector 的 HTTP Receiver 地址,相关字段可参考 <a href="https://apisix.apache.org/zh/docs/apisix/next/plugins/opentelemetry/">Apache APISIX 官方文档</a>。</p> <pre><code class="language-YAML">plugins ... # 已经启用的其它插件 - opentelemetry plugin_attr: ... opentelemetry: trace_id_source: x-request-id resource: service.name: APISIX collector: address: 127.0.0.1:4318 # OTLP HTTP Receiver 地址 request_timeout: 3 </code></pre> <h4>方法一:将插件绑定到指定路由</h4> <p>为了更方便的展示测试效果,示例中暂时将 <code>sampler</code> 设置为全采样,以确保每次请求都被追踪后产生 <code>trace</code> 数据,方便您在 Web UI 上查看 <code>trace</code> 的相关数据。您也可以根据实际情况,设置相关参数。</p> <pre><code class="language-Shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \ -X PUT -d ' { "uri": "/get", "plugins": { "opentelemetry": { "sampler": { "name": "always_on" } } }, "upstream": { "type": "roundrobin", "nodes": { "httpbin.org:80": 1 } } }' </code></pre> <h4>方式二:设置 Global Rules</h4> <p>您也可以通过 Apache APISIX Plugins 功能启用 <code>opentelemetry</code> 插件。完成全局配置后,您仍然需要创建路由,否则将无法进行测试。</p> <pre><code class="language-Shell">curl 'http://127.0.0.1:9080/apisix/admin/global_rules/1' \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \ -X PUT -d '{ "plugins": { "opentelemetry": { "sampler": { "name": "always_on" } } } }' </code></pre> <h4>方式三:通过 <code>additional_attributes</code> 为 Span 自定义标签</h4> <p>关于 <code>sampler</code> 和 <code>additional_attributes</code> 的配置您可以参考 <a href="https://apisix.apache.org/zh/docs/apisix/next/plugins/opentelemetry/">Apache APISIX 官方文档</a>,其中 <code>additional_attributes</code> 是一系列的 <code>Key:Value</code> 键值对,您可以使用它为 Span 自定义标签,并且可以跟随 Span 在 Web UI 上展示。通过 <code>additional_attributes</code> 为某个路由的 Span 增加 <code>route_id</code> 和 <code>http_x-custom-ot-key</code>,可以参考如下配置:</p> <pre><code class="language-Shell">curl http://127.0.0.1:9080/apisix/admin/routes/1001 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \ -X PUT -d ' { "uri": "/put", "plugins": { "opentelemetry": { "sampler": { "name": "always_on" }, "additional_attributes":[ "route_id", "http_x-custom-ot-key" ] } }, "upstream": { "type": "roundrobin", "nodes": { "httpbin.org:80": 1 } } }' </code></pre> <h3>测试示例</h3> <p>您可以通过以上三种方式中的任意一种方式启用 <code>opentelemetry</code>,以下示例使用方式三的方法创建路由,创建成功后,您可以参考如下命令访问路由:</p> <pre><code class="language-Shell">curl -X PUT -H `x-custom-ot-key: test-ot-val` http://127.0.0.1:9080/put </code></pre> <p>访问成功之后,您可以在 Jaeger UI 中看到类似如下图所示 /put 的 Span 详情,并可以看到 Tags 列表中展示了路由中自定义的 tag:<code>http_x-custom-ot-key</code> 和 <code>route_id</code>。</p> <p><img src="https://static.apiseven.com/202108/1646039676695-a346734b-0498-4ff6-8882-789a61008544.png" alt="error/Span details.png" referrerpolicy="no-referrer"></p> <p>您需要注意,<code>additional_attributes</code> 配置的设定是从 Apache APISIX 和 Nginx 变量取值作为 <code>attribute</code> 的值,因此 <code>additional_attributes</code> 必须是 Apache APISIX 或者 Nginx 的有效变量。其中也包括 HTTP Header,但是在取 <code>http_header</code> 时,需要添加 <code>http_</code> 作为变量名的前缀。如果变量不存在,就不会展示这个 <code>tag</code> 了。</p> <h3>场景示例</h3> <p>本场景示例通过简单修改 OpenTelemetry Collector 官方示例部署 Collector、Jaeger 和 Zipkin 作为后端服务,并且启动两个示例应用程序(Client 和 Server),其中 Server 提供了一个 HTTP 服务,而 Client 会循环调用 Server 提供的 HTTP 接口,从而产生包括两个 Span 的调用链。</p> <h4>步骤一:部署 OpenTelemetry</h4> <p>以下使用 <code>docker-compose</code> 作为示例,其它部署可以参考 <a href="https://opentelemetry.io/docs/collector/getting-started/">OpenTelemetry 官方文档</a>。</p> <p>您可以参考如下命令部署:</p> <pre><code class="language-Shell">git clone https://github.com/open-telemetry/opentelemetry-collector-contrib.git cd opentelemetry-collector-contrib/examples/demo docker-compose up -d </code></pre> <p>在浏览器中输入 <code>http://127.0.0.1:16886</code>(Jaeger UI)或者 <code>http://127.0.0.1:9411/zipkin</code>(Zipkin UI),如果可以正常访问,则表示部署成功。</p> <p>下图为访问成功示例:</p> <p><img src="https://static.apiseven.com/202108/1646039980335-71bbb6f7-39d5-4153-b6e7-0305f52112f3.png" alt="error/Jaeger example.png" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/202108/1646040117233-7a18f85f-4037-43e3-bc63-0ff6d1dbe5c1.png" alt="error/Zipkin example.png" referrerpolicy="no-referrer"></p> <h4>步骤二:配置测试环境</h4> <p>引入 Apache APISIX 服务,最终应用的拓扑如下图所示:</p> <p><img src="https://static.apiseven.com/202108/1646040225319-819f10ab-9643-4bd7-8f99-07f9a6c84bf8.png" alt="error/Architecture diagram.png" referrerpolicy="no-referrer"></p> <p>Trace 数据上报流程如下。其中由于 Apache APISIX 是单独部署的,并不在 <code>docker-compose</code> 的网络内,所以 Apache APISIX 是通过本地映射的端口(即 <code>127.0.0.1:4138</code>)访问到 OpenTelemetery Collector 的 OTLP HTTP Receiver 的。</p> <p><img src="https://static.apiseven.com/202108/1646040470172-4d44c6ca-b890-4245-9c87-3a42d8b59f47.png" alt="error/Trace data reporting process.png" referrerpolicy="no-referrer"></p> <p>您需要确保已经启用 <code>opentelemetry</code> 插件,并重新加载 Apache APISIX。</p> <ol> <li>您可以参考如下示例创建一个路由,并且启用 <code>opentelemetry</code> 插件进行采样:</li> </ol> <pre><code class="language-Shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \ -X PUT -d ' { "uri": "/hello", "plugins": { "opentelemetry": { "sampler": { "name": "always_on", } } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:7080": 1 } } }' </code></pre> <ol start="2"> <li>修改 <code>./examples/demo/otel-collector-config.yaml</code> 文件,并添加 OTLP HTTP Receiver,如下所示:</li> </ol> <pre><code class="language-Shell">receivers: otlp: protocols: grpc: http:${ip:port} # 添加 OTLP HTTP Receiver,默认端口为 4318 </code></pre> <ol start="3"> <li>修改 <code>docker-compose.yaml</code>。</li> </ol> <p>您需要修改配置文件,把 Client 调用 Server 的接口地址修改为 Apache APISIX 的地址,将 OTLP HTTP Receiver 和 Server 服务的端口映射到本地。</p> <p>以下示例是修改配置后完整的 <code>docker-compose.yaml</code>:</p> <pre><code class="language-YAML">version: "2" services: # Jaeger jaeger-all-in-one: image: jaegertracing/all-in-one:latest ports: - "16686:16686" # jaeger ui 的端口 - "14268" - "14250" # Zipkin zipkin-all-in-one: image: openzipkin/zipkin:latest ports: - "9411:9411" # Collector otel-collector: image: ${OTELCOL_IMG} command: ["--config=/etc/otel-collector-config.yaml", "${OTELCOL_ARGS}"] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml ports: - "1888:1888" # pprof extension - "8888:8888" # Prometheus metrics exposed by the collector - "8889:8889" # Prometheus exporter metrics - "13133:13133" # health_check extension - "4317" # OTLP gRPC receiver - "4318:4318" # 添加 OTLP HTTP Receiver 端口映射 - "55670:55679" # zpages extension depends_on: - jaeger-all-in-one - zipkin-all-in-one demo-client: build: dockerfile: Dockerfile context: ./client environment: - OTEL_EXPORTER_OTLP_ENDPOINT=otel-collector:4317 - DEMO_SERVER_ENDPOINT=http://172.17.0.1:9080/hello # APISIX 的地址 depends_on: - demo-server demo-server: build: dockerfile: Dockerfile context: ./server environment: - OTEL_EXPORTER_OTLP_ENDPOINT=otel-collector:4317 ports: - "7080:7080" # 将 Server 端口映射到宿主机 depends_on: - otel-collector prometheus: container_name: prometheus image: prom/prometheus:latest volumes: - ./prometheus.yaml:/etc/prometheus/prometheus.yml ports: - "9090:9090" </code></pre> <p>需要注意,<code>demo-client.environment.DEMO_SERVER_ENDPOINT</code> 处需要改为您的 Apache APISIX 地址,且保证在容器内可以正常访问。</p> <p>当然,您也可以通过 <code>docekr-compose.yaml</code> 部署 Apache APISIX ,具体可以参考 <a href="https://github.com/apache/apisix-docker/blob/master/docs/en/latest/example.md">Apache APISIX 官方文档</a>。</p> <h4>步骤三:测试</h4> <p>重新部署完成后,访问 Jaeger UI 或者 Zipkin UI 即可看到 Trace 中包含了 APISIX 的 Span,如下图:</p> <p><img src="https://static.apiseven.com/202108/1646045290844-acfa071b-5a0d-4f7a-aa77-55838a3cb9f6.png" alt="error/Jaeger example.png" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/202108/1646045376329-e1344754-58b4-4a73-8aea-50e6a04f3b70.png" alt="error/Zipkin example.png" referrerpolicy="no-referrer"></p> <h2>禁用插件</h2> <p>如果您暂时不需要某个路由的 Trace 采集,则仅需修改路由配置,将配置中的 <code>plugins</code> 下的 <code>opentelemetry</code> 相关配置删除即可。</p> <p>如果您是通过绑定 Global Rules 全局启用的,则只能删除 <code>opentelemetry</code> 全局插件的配置。得益于 Apache APISIX 的动态化优势,开启关闭插件的过程都不需要重启 Apache APISIX,十分方便。</p> <h2>总结</h2> <p>Apache APISIX 在集成 OpenTelemetery 之后,借助 OpenTelemetry 丰富的插件能够与市场上大部分主流的 Trace 系统轻松实现对接。此外,Apache APISIX 也实现了 SkyWalking 和 Zipkin 原生标准协议插件,也在积极与各大社区合作打造更加强大的生态。</p> <p>Apache APISIX 项目目前正在开发其他插件以支持集成更多服务,如果您对此有兴趣,您可以通过 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 发起讨论,或通过<a href="https://apisix.apache.org/docs/general/subscribe-guide">邮件列表</a>进行交流.</p> <h2>相关阅读</h2> <ul> <li> <p><a href="https://apisix.apache.org/zh/blog/2021/11/04/skywalking">浅谈 Apache APISIX 的可观测性</a></p> </li> <li> <p><a href="https://apisix.apache.org/zh/blog/2022/02/10/splunk-apisix-integration/">生态扩大进行中!API 网关集成 Splunk HTTP Event Collector</a></p> </li> </ul>

Apache APISIX 集成 Consul KV,服务发现能力再升级

<h2>背景信息</h2> <p>Consul 是一个服务网格解决方案,其核心之一的 Consul KV 是一个分布式键值数据库,主要用途是存储配置参数和元数据,同时也允许用户存储索引对象。</p> <p>在微服务架构模式下,当扩容、硬件故障等导致上游服务发生变动的情况出现时,通过手动撰写配置,维护上游服务信息的方式,会导致维护成本陡增。对此,Apache APISIX 提供了服务发现注册中心的功能,实现动态获取最新的服务实例信息,以降低用户的维护成本。</p> <p>目前,Apache APISIX 借由社区贡献的 <code>consul_kv</code> 模块,支持了基于 Consul KV 的服务发现注册中心。</p> <h2>插件工作原理</h2> <p>Apache APISIX 利用 Consul KV 分布式键值存储能力的 <code>consul_kv</code> 模块,解耦了服务的提供者和消费者,实现了服务发现注册中心的两大核心功能。</p> <ol> <li>服务注册:服务提供者向注册中心注册服务。</li> <li>服务发现:服务消费者通过注册中心查找服务提供者的路由信息。</li> </ol> <p>在此基础上构建的 Apache APISIX 将更灵活地适应现有的微服务架构,更好地满足用户需求。</p> <p><img src="https://static.apiseven.com/202108/1645769130815-f23e9e11-ca57-4262-9083-aab5509aa178.png" alt="APISIX Consul 工作原理" referrerpolicy="no-referrer"></p> <h2>如何使用</h2> <p>本文中的测试环境均使用 docker-compose 在 Docker 中搭建。</p> <ol> <li>下载 Apache APISIX。</li> </ol> <pre><code class="language-shell"># 拉取 apisix-docker 的 Git 仓库 git clone https://github.com/apache/apisix-docker.git </code></pre> <ol start="2"> <li>创建 Consul 文件夹和配置文件。</li> </ol> <pre><code class="language-shell"># 创建 consul 文件夹 mkdir -p ~/docker-things/consul/ &amp;&amp; cd "$_" # 创建配置文件 touch docker-compose.yml server1.json </code></pre> <ol start="3"> <li>修改 <code>docker-compose.yml</code> 文件。</li> </ol> <pre><code class="language-yaml">version: '3.8' services: consul-server1: image: consul:1.9.3 container_name: consul-server1 restart: always volumes: - ./server1.json:/consul/config/server1.json:ro networks: - apisix ports: - '8500:8500' command: 'agent -bootstrap-expect=1' networks: apisix: external: true name: example_apisix </code></pre> <ol start="4"> <li>修改 <code>server1.json</code> 文件。</li> </ol> <pre><code class="language-json">{ "node_name": "consul-server1", "server": true, "addresses": { "http": "0.0.0.0" } } </code></pre> <ol start="5"> <li>在 Apache APISIX 中的配置文件 <code>apisix_conf/config.yaml</code> 添加 Consul 的相关配置信息。</li> </ol> <pre><code class="language-yaml"># config.yml # ...other config discovery: consul_kv: servers: - "http://consul-server1:8500" prefix: "upstreams" </code></pre> <ol start="6"> <li>启动 Apache APISIX 和 Consul。</li> </ol> <pre><code class="language-shell"># 进入 example 和 consul 文件夹,依次启动 APISIX 和 Consul docker-compose up -d </code></pre> <ol start="7"> <li>将测试服务注册到 Consul。<code>example</code> 包含了两个 Web 服务,你可以直接使用这两个服务进行测试。</li> </ol> <pre><code class="language-shell"># 检查 example 的 docker-compose.yml 就可以看到两个 Web 服务 $ cat docker-compose.yml | grep web # 输出 web1: - ./upstream/web1.conf:/etc/nginx/nginx.conf web2: - ./upstream/web2.conf:/etc/nginx/nginx.conf </code></pre> <ol start="8"> <li>确认 Web 服务的 IP 地址。</li> </ol> <pre><code class="language-shell">$ sudo docker inspect -f='{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(sudo docker ps -aq) | grep web # 输出 /example-web1-1 - 172.26.0.7 /example-web2-1 - 172.26.0.2 </code></pre> <ol start="9"> <li>在终端中对 Consul 的 HTTP API 进行请求以注册测试服务。</li> </ol> <pre><code class="language-shell"># 使用对应的 IP 进行注册 curl \ -X PUT \ -d ' {"weight": 1, "max_fails": 2, "fail_timeout": 1}' \ http://127.0.0.1:8500/v1/kv/upstreams/webpages/172.26.0.7:80 curl \ -X PUT \ -d ' {"weight": 1, "max_fails": 2, "fail_timeout": 1}' \ http://127.0.0.1:8500/v1/kv/upstreams/webpages/172.26.0.2:80 </code></pre> <p>其中,<code>/v1/kv/</code> 后的路径按照 <code>{Prefix}/{Service Name}/{IP}:{Port}</code> 的格式构成。</p> <p><code>{Prefix}</code> 是在 APISIX 中配置 Consul 时写入的 prefix,<code>{Service Name}</code> 和 <code>{IP}:{Port}</code> 则需要根据上游服务,由用户自行确定。</p> <p>而数据的格式则为 <code>{"weight": &lt;Num&gt;, "max_fails": &lt;Num&gt;, "fail_timeout": &lt;Num&gt;}</code>。</p> <ol start="10"> <li>查看测试服务是否注册成功。</li> </ol> <pre><code class="language-shell">$ curl "http://127.0.0.1:8500/v1/kv/upstreams/webpages?keys" </code></pre> <p>返回消息如下则表示注册成功。</p> <pre><code class="language-shell">["upstreams/webpages/172.26.0.2:80","upstreams/webpages/172.26.0.7:80"]% </code></pre> <h3>创建路由并为其启用 Consul</h3> <p>使用 Apache APISIX 提供的 Admin API 将 Consul 添加到路由中。</p> <p>在添加之前需要先确定 <code>X-API-KEY</code> 和 <code>upstream.service_name</code> 两个数据。</p> <ul> <li><code>X-API-KEY</code>:Admin API 的访问 Token,在此示例中,我们使用默认的 <code>edd1c9f034335f136f87ad84b625c8f1</code> 即可。</li> <li><code>upstream.service_name</code>:上游服务的名称,它指定了将与某个路由绑定的某个注册中心中的服务(Service),使用 Consul 时需要设置为注册测试服务时的 URL,并去掉最后的 <code>{IP}:{Port}</code> 部分。我们也可以通过 Apache APISIX 提供的 Memory Dump API 获取服务的 URL,同时确认是否能正常发现上游服务。</li> </ul> <pre><code class="language-shell">$ curl http://127.0.0.1:9092/v1/discovery/consul_kv/dump | jq # 输出 { "services": { # 这个 key 就是需要的 URL "http://consul-server1:8500/v1/kv/upstreams/webpages/": [ { "port": 80, "host": "172.26.0.7", "weight": 1 }, { "port": 80, "host": "172.26.0.2", "weight": 1 } ] }, "config": { # ...configs } } </code></pre> <h3>添加路由</h3> <p>这里将 URL 为 <code>/consul/*</code> 的请求路由分配到 <code>http://consul-server1:8500/v1/kv/upstreams/webpages/</code>。同时, <code>discovery_type</code> 必须设置为 <code>consul_kv</code> 以启动对应模块。</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X POST -d ' { "uri": "/consul/*", "upstream": { "service_name": "http://consul-server1:8500/v1/kv/upstreams/webpages/", "type": "roundrobin", "discovery_type": "consul_kv" } }' </code></pre> <h3>测试配置结果</h3> <p>通过请求结果可以看到, Apache APISIX 中新增的路由已经可以通过 Consul 找到正确的服务地址,并根据负载均衡策略请求到两个节点上。</p> <pre><code class="language-shell"># 第一次请求 curl -s http://127.0.0.1:9080/consul/ # 输出 hello web1% # 第二次请求 curl -s http://127.0.0.1:9080/consul/ # 输出 hello web2% # 注意:也有可能两次请求获取的都是 web1 或者 web2。 # 这是由负载均衡的特性造成的,您可以尝试进行更多次请求。 </code></pre> <h2>总结</h2> <p>本文的前半部分介绍了 Apache APISIX 如何配合 Consul 实现基于 Consul KV 的服务发现注册中心,解决服务信息管理维护的问题。而在后半部分则着重介绍了如何在 Docker 中搭配 Consul 使用 Apache APISIX 的操作流程。当然,在实际场景中的应用,还需要各位读者根据业务使用场景和已有的系统架构具体分析。关于在 Apache APISIX 中使用 Consul 注册中心的更多说明,可以在<a href="https://apisix.apache.org/zh/docs/apisix/discovery/consul_kv/">官方文档</a>中找到。</p> <p>Apache APISIX 项目目前正在开发其他插件以支持集成更多服务,如果您对此有兴趣,您可以通过 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 发起讨论,或通过<a href="https://apisix.apache.org/docs/general/subscribe-guide">邮件列表</a>进行交流.</p>

如何使用 Apache APISIX CSRF 安全插件拦截跨站点伪造攻击

<p>CSRF(Cross-Site Request Forgery),即跨站点请求伪造。发起跨站点请求伪造攻击的关键点在于让目标服务器无法分辨众多请求的来源是真实用户还是攻击者。攻击的一般流程为:首先攻击者会诱导用户导航至攻击者提供的网页上。该网页包含一个自动发送到目标服务器的请求。然后该网页正常加载,这个请求就会自动发送至服务器。在服务器看来,这个请求和用户正常发送的请求一模一样,殊不知这是由攻击者发起,而用户却毫不知情。由于该请求携带了用户的一些凭据,攻击者通过解析这些凭据,就可以获取用户信息,进而产生安全风险。</p> <p>本文介绍了 Apache APISIX 的 CSRF 安全插件 <code>csrf</code>,并详细说明如何在 Apache APISIX 中借助 <code>csrf</code> 插件来保护您的 API 信息安全。</p> <h2>插件介绍</h2> <p><code>csrf</code> 插件基于 <code>Double Submit Cookie</code> 方案实现。根据 <a href="https://datatracker.ietf.org/doc/html/rfc7231.html#section-4.2.1">RFC 7231#section-4.2.1</a> 的定义,我们把 <code>GET</code>、<code>HEAD</code> 和 <code>OPTIONS</code> 这三种方法称为安全方法。按照这一约定,<code>csrf</code> 插件会直接放行这三种方法,但会对其他的方法做检查并拦截其中的不安全请求。</p> <p>为了抵御 CSRF 攻击,我们需要制造一个无法伪造的令牌或标识符,并且保证这个不会与攻击者的请求一起发送。用户需要将 <code>csrf</code> 插件依赖的 token 携带在请求头,token 使用密钥进行签名计算。这样就保证了 token 无法被他人伪造,从而保证了 API 的安全。</p> <p><img src="https://static.apiseven.com/202108/1645605178661-7c0bc3bc-9792-43fd-b3f6-b01c0f6b24db.png" alt="插件工作流程" referrerpolicy="no-referrer"></p> <p>在路由中开启 <code>csrf</code> 插件后,访问该路由的所有请求响应中都会包含携带了 <code>csrf token</code> 的 Cookie。</p> <p>用户需要在对该路由的不安全请求中携带这一 Cookie,并在请求头添加额外的字段用来携带 Cookie 的内容。字段为插件配置中的 <code>name</code> 值,这样的请求才能通过 CSRF 插件的校验。</p> <p>用户在插件的配置中提供一个随机密钥,插件使用该密钥对 token 信息进行 sha256 哈希加密,然后生成 CSRF token,从而保证该 token 不可伪造。</p> <h2>如何使用</h2> <h3>配置开启 CSRF 插件的路由</h3> <p>在 APISIX 中使用 Admin API 创建一条路由并启用 <code>csrf</code> 插件:</p> <pre><code class="language-shell">curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "/hello", "plugins": { "csrf": { "key": "edd1c9f034335f136f87ad84b625c8f1" } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:9001": 1 } } }' </code></pre> <p>其中对于插件有三个配置项:</p> <ul> <li><code>key</code>:必填项,随机秘钥的值。用户需要提供一个随机密钥。</li> <li><code>expires</code>:选填项,随机秘钥的过期时间,默认值为 7200 秒。由于 CSRF token 使用 Cookie 下发至客户端,该配置会被放置在 Cookie 的配置中,从而控制 Cookie 的过期时间。另外在插件内部也会计算时间来判断 token 是否过期。</li> <li><code>name</code>:选填项, CSRF token 的名称,默认值为 <code>apisix-csrf-token</code>。</li> </ul> <h3>发送请求测试</h3> <p>首先使用 <code>POST</code> 请求访问该路由:</p> <pre><code class="language-shell">curl -i http://127.0.0.1:9080/hello -X POST </code></pre> <p>Apache APISIX 会拦截该请求并返回 <code>401</code> 错误。在返回的头部中会发现设置了一个 Cookie,如果没有配置插件的 <code>name</code> 字段的话,Cookie 内部应该为默认值 <code>apisix-csrf-token=....</code> 。这就是 CSRF 插件生成的 CSRF token。在请求中,需要确保请求携带该 Cookie 并且在请求头写入该 token。</p> <pre><code class="language-shell">HTTP/1.1 401 Unauthorized Set-Cookie: apisix-csrf-token= ${apisix-csrf-token};path=/;Expires=Mon, 13-Dec-21 09:33:55 GMT {"error_msg":"no csrf token in headers"} </code></pre> <p>客户端使用 JavaScript 示例:使用 <code>js-cookie</code> 读取 Cookie 并使用 <code>axios</code> 发送请求。</p> <pre><code class="language-js">const token = Cookie.get('apisix-csrf-token'); const instance = axios.create({ headers: {'apisix-csrf-token': token} }); </code></pre> <p>如果 Cookie 中的 token 和请求头中的 token 不一致,请求会被 <code>csrf</code> 插件拦截,示例如下:</p> <pre><code class="language-shell">curl -i http://127.0.0.1:9080/hello -X POST -H 'apisix-csrf-token: ${apisix-csrf-token}' -b 'apisix-csrf-token= ${apisix-csrf-token}' </code></pre> <pre><code class="language-shell">HTTP/1.1 401 Unauthorized Set-Cookie: apisix-csrf-token= ${apisix-csrf-token};path=/;Expires=Mon, 13-Dec-21 09:33:55 GMT {"error_msg":"csrf token mismatch"} </code></pre> <p>最后使用 <code>curl</code> 验证正常的访问:</p> <pre><code class="language-shell">curl -i http://127.0.0.1:9080/hello -X POST -H 'apisix-csrf-token: ${apisix-csrf-token}' -b 'apisix-csrf-token= ${apisix-csrf-token}' </code></pre> <pre><code class="language-shell">HTTP/1.1 200 OK </code></pre> <p>插件在内部需要校验 Cookie 中的 token 与 请求头中携带 token 的是否一致,并对其进行重新计算签名来验证该 token 是否有效。</p> <h3>禁用插件</h3> <p>移除 <code>csrf</code> 插件的相关配置信息,然后发送一个更新路由的请求,即可停用该插件。</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "/hello", "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } } }' </code></pre> <h2>总结</h2> <p>本文详细描述了 <code>csrf</code> 插件的工作方式以及使用方法,希望通过本文可以让大家对在 Apache APISIX 中使用插件拦截 CSRF 攻击有更清晰的认识,方便在实际场景中应用。</p> <p>Apache APISIX 项目目前正在开发其他插件以支持集成更多服务,如果您对此有兴趣,您可以通过 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 发起讨论,或通过<a href="https://apisix.apache.org/docs/general/subscribe-guide">邮件列表</a>进行交流.</p>

Nacos 在 API 网关中的服务发现实践

<h2>背景信息</h2> <p>Apache APISIX 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。它不仅拥有众多实用的插件,而且支持插件动态变更和热插拔。同时在使用服务发现组件时,不仅可以利用 etcd,也可以将 Eureka、Consul 和 Nacos 作为服务发现组件。本文将详细为您介绍如何在 Apache APISIX 中配置 Nacos 作为 Apache APISIX API 网关中的服务发现组件。</p> <p><img src="https://static.apiseven.com/202108/1646038041730-3d9bfdd8-d2f0-41a2-84f5-cb1e1d567a86.png" alt="error/Apache APISIX API Getway.png" referrerpolicy="no-referrer"></p> <p>注册中心是服务要实现服务化管理的核心组件,类似于目录服务的作用,也是微服务架构中最基础的设施之一,主要用来存储服务信息,譬如服务提供者 URL 、路由信息等。注册中心的实现是通过一种映射的方式,将复杂的服务端信息映射为简单易懂的信息提供给客户端。</p> <p>注册中心的核心功能为以下三点:</p> <ul> <li>服务注册:<strong>服务提供方</strong>向<strong>注册中心</strong>进行注册。</li> <li>服务发现:<strong>服务消费方</strong>可以通过注册中心寻找到服务提供方的调用路由信息。</li> <li>健康检测:确保注册到注册中心的服务节点是可以被正常调用的,避免无效节点导致的调用资源浪费等问题。</li> </ul> <p>注册中心本质上是为了<strong>解耦服务提供者和服务消费者</strong>,在微服务体系中,各个业务服务之间会频繁互相调用,并且需要对各个服务的 IP、port 等路由信息进行统一的管理。但是要如何进行管理呢?我们可以通过注册中心的<strong>服务注册</strong>功能将已有服务的相关信息提供到统一的注册中心进行管理。</p> <p>通过上述描述,您可以了解到注册中心可以帮助用户通过映射快速找到服务和服务地址。随着业务更新迭代,服务会频繁发生变化,在服务端中注册了新的服务或者服务宕机后,客户端仍然可以通过注册中心的<strong>服务发现</strong>功能拉取服务列表,如果注册中心的服务节点发生变更,注册中心会发送请求通知客户端重新拉取。</p> <p>如果服务端的服务突然宕机,并且没有向注册中心反馈,客户端可以通过注册中心的<strong>健康检查</strong>功能,进行固定时间间隔的主动上报心跳方式向服务端表明自己的服务状态。如果服务状态异常,则会通知注册中心,注册中心可以及时把已经宕机的服务节点进行剔除,避免资源的浪费。</p> <p>Apache APISIX + Nacos 可以将各个微服务节点中与业务无关的各项控制,集中在 Apache APISIX 中进行统一管理,即<strong>通过 Apache APISIX 实现接口服务的代理和路由转发的能力</strong>。在 Nacos 上注册各个微服务后,Apache APISIX 可以通过 Nacos 的服务发现功能获取服务列表,查找对应的服务地址从而实现动态代理。</p> <p><img src="https://static.apiseven.com/202108/1645433492822-5218e923-97ae-4d04-863b-3b3f901de84f.png" alt="error/Principle Introduction.png" referrerpolicy="no-referrer"></p> <h2>API 网关基于 Nacos 实现服务发现</h2> <h3>前提条件</h3> <p>本文操作基于以下环境进行。</p> <ul> <li>操作系统 Centos 7.9。</li> <li>已安装 Apache APISIX 2.12.1,详情请参考:<a href="https://apisix.apache.org/zh/docs/apisix/how-to-build">如何构建 Apache APISIX</a>。</li> <li>已安装 Nacos 2.0.4,详情请参考:<a href="https://nacos.io/zh-cn/docs/quick-start.html">Nacos 快速入门</a>。</li> <li>已安装 Node.js,详情请参考:<a href="https://github.com/nodejs/help/wiki/Installation">Node.js 安装指南</a>。</li> </ul> <h3>步骤一:服务注册</h3> <ol> <li>使用 Node.js 的 Koa 框架在 <code>3005</code> 端口启动一个简单的测试服务作为<a href="https://apisix.apache.org/zh/docs/apisix/admin-api#upstream">上游(Upstream)</a>。</li> </ol> <pre><code class="language-JavaScript">const Koa = require('koa'); const app = new Koa(); app.use(async ctx =&gt; { ctx.body = 'Hello World'; }); app.listen(3005); </code></pre> <ol start="2"> <li>在命令行中通过请求 Nacos Open API 的方式进行服务注册。</li> </ol> <pre><code class="language-Shell">curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=APISIX-NACOS&amp;ip=127.0.0.1&amp;port=3005&amp;ephemeral=false' </code></pre> <ol start="3"> <li>执行服务注册后使用以下命令查询当前服务情况。</li> </ol> <pre><code class="language-Shell">curl -X GET 'http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=APISIX-NACOS' </code></pre> <p>正确返回结果示例如下:</p> <pre><code class="language-JSON">{ "name": "DEFAULT_GROUP@@APISIX-NACOS", "groupName": "DEFAULT_GROUP", "clusters": "", "cacheMillis": 10000, "hosts": [ { "instanceId": "127.0.0.1#3005#DEFAULT#DEFAULT_GROUP@@APISIX-NACOS", "ip": "127.0.0.1", "port": 3005, "weight": 1.0, "healthy": true, "enabled": true, "ephemeral": true, "clusterName": "DEFAULT", "serviceName": "DEFAULT_GROUP@@APISIX-NACOS", "metadata": {}, "instanceHeartBeatInterval": 5000, "instanceHeartBeatTimeOut": 15000, "ipDeleteTimeout": 30000, "instanceIdGenerator": "simple" } ], "lastRefTime": 1643191399694, "checksum": "", "allIPs": false, "reachProtectionThreshold": false, "valid": true } </code></pre> <h3>步骤二:新增 Nacos 路由</h3> <p>使用 Apache APISIX 提供的 Admin API 创建一个新的<a href="https://apisix.apache.org/zh/docs/apisix/admin-api#route">路由(Route)</a>,APISIX 通过 <code>upstream.discovery_type</code> 字段选择使用的服务发现类型,<code>upstream.service_name</code> 需要与注册中心的对应服务名进行关联,因此创建路由时指定服务发现类型为 <code>nacos</code>。</p> <pre><code class="language-Shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' { "uri": "/nacos/*", "upstream": { "service_name": "APISIX-NACOS", "type": "roundrobin", "discovery_type": "nacos" } }' </code></pre> <p>在上述命令中,请求头 <code>X-API-KEY</code> 是 Admin API 的访问 token,可以在 <code>conf/config.yaml</code> 文件中的 <code>apisix.admin_key.key</code> 查看。</p> <p>添加成功后,正确返回结果示例如下:</p> <pre><code class="language-JSON">{ "action": "set", "node": { "key": "\/apisix\/routes\/1", "value": { "update_time": 1643191044, "create_time": 1643176603, "priority": 0, "uri": "\/nacos\/*", "upstream": { "hash_on": "vars", "discovery_type": "nacos", "scheme": "http", "pass_host": "pass", "type": "roundrobin", "service_name": "APISIX-NACOS" }, "id": "1", "status": 1 } } } </code></pre> <p>除此之外,您还可以在 <code>upstream.discovery_args</code> 中传递其他服务相关参数用于指定服务所在的命名空间或组别,具体内容可参考<a href="https://apisix.apache.org/zh/docs/apisix/next/discovery/nacos/#%E5%8F%82%E6%95%B0">官方文档</a>。</p> <h3>步骤三:验证配置结果</h3> <p>使用以下命令发送请求至需要配置的路由。</p> <pre><code class="language-Shell">curl -i http://127.0.0.1:9080/nacos/ </code></pre> <p>正常返回结果示例如下:</p> <pre><code class="language-Shell">HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 11 Connection: keep-alive Date: Thu, 27 Jan 2022 00:48:26 GMT Server: APISIX/2.12.0 Hello World </code></pre> <p>通过示例看到,Apache APISIX 中新增的路由已经可以通过 Nacos 服务发现找到正确的服务地址,并正常响应。</p> <h2>总结</h2> <p>本文为大家介绍了注册中心的概念以及 Apache APISIX 如何配合 Nacos 实现基于服务发现的路由代理。用户可以根据自身的业务需求和过往技术架构使用 Apache APISIX 与 Nacos,以实现接口服务的代理和路由转发的能力。</p> <p>关于 <code>nacos</code> 插件的更多说明和完整配置信息,可参考 <a href="https://apisix.apache.org/zh/docs/apisix/discovery/nacos">Apache APISIX 官方文档</a>。</p> <p>Apache APISIX 项目目前正在开发其他插件以支持集成更多服务,如果您对此有兴趣,您可以通过 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 发起讨论,或通过<a href="https://apisix.apache.org/docs/general/subscribe-guide">邮件列表</a>进行交流.</p>

后端新手如何从 0 到 1 打造一款 Apache APISIX 插件

<p>在过去的几个月,社区用户为 Apache APISIX 添加了许多插件,丰富了 Apache APISIX 的生态。从使用者的角度而言,更多样化的插件出现无疑是一件好事,它们在完善 Apache APISIX 高性能和低延迟的基础之上,满足了使用者对于网关的更多期望,即“一站式”和“多功能”。</p> <p>社区的贡献者们是如何为 Apache APISIX 开发插件的呢?Apache APISIX 博客上的文章似乎都没有详细讲述过开发插件的流程。那么这次我们换一个视角,从插件开发者的角度出发,一起来看看一款插件诞生的全过程吧!</p> <p>本篇文章记录了一个<strong>没有后端经验的前端工程师</strong>开发 <code>file-logger</code> 插件的过程。在详细说明实现过程之前,先向大家简单介绍下 <code>file-logger</code> 的功能。</p> <h2>功能介绍</h2> <p><code>file-logger</code> 支持使用 Apache APISIX 插件元数据生成自定义的日志格式,用户可以通过 <code>file-logger</code> 插件将 JSON 格式的请求和响应数据附加到日志文件中,也可以将 Log 数据流推送到指定位置。</p> <p>试想,在监控某一个路由的访问日志时,很多时候我们关注的不仅是某些请求和响应数据的值,还希望将日志数据单独写入到指定的文件中。这时就可以使用 <code>file-logger</code> 插件来帮忙实现这些需求。</p> <p><img src="https://static.apiseven.com/202108/1644996258131-a0e32942-dcc5-4129-873f-0a7551532e77.png" alt="功能介绍" referrerpolicy="no-referrer"></p> <p>具体实现过程中我们可以通过 <code>file-logger</code> 将日志数据单独写入指定的日志文件,简化监控和调试的过程。</p> <h2>开发实现过程</h2> <p>介绍完 <code>file-logger</code> 的功能之后,大家也算对这个插件有了更多的认识。下面为大家详细讲解一下,<strong>没有服务端经验</strong>的我是如何从 0 开始为 Apache APISIX 完成该插件并添加相应测试的。</p> <h3>确定插件名称及优先级</h3> <p>打开 <a href="https://apisix.apache.org/zh/docs/apisix/plugin-develop/#%E6%8F%92%E4%BB%B6%E5%91%BD%E5%90%8D%EF%BC%8C%E4%BC%98%E5%85%88%E7%BA%A7%E5%92%8C%E5%85%B6%E4%BB%96">Apache APISIX 插件开发指南</a>,按照先后顺序需要确定以下几项:</p> <ol> <li>确定插件分类。</li> <li>确定插件优先级,并更新 <code>conf/config-default.yaml</code> 文件。</li> </ol> <p>因为此次开发的 <code>file-logger</code> 属于日志类型的插件,所以我参考了 Apache APISIX 已有的日志插件的名称和排序,将 <code>file-logger</code> 放到了这里:</p> <p><img src="https://static.apiseven.com/202108/1644996436166-58305d35-3798-4df2-b8df-1874f3e0cb01.png" alt="确认 file-logger 插件名称和优先级" referrerpolicy="no-referrer"></p> <p>咨询了其他插件的作者和社区的热心成员之后,最终确认了该插件的名称 <code>file-logger</code> 和优先级 <code>399</code>。</p> <blockquote> <p>需要注意的是,插件的优先级与执行顺序有关,优先级的值越大,执行越靠前。而插件名称的排序与执行顺序无关。</p> </blockquote> <h3>创建最小可执行插件文件</h3> <p>确认好插件名称和优先级后,便可在 <code>apisix/plugins/</code> 目录下创建我们的插件代码文件,这里有两点需要注意:</p> <ul> <li>如果是直接在 <code>apisix/plugins/</code> 目录下直接创建插件代码文件,就无需更改 <code>Makefile</code> 文件。</li> <li>如果你的插件有新建自己的代码目录,那么就需要更新 <code>Makefile 文件</code>,详细步骤可参考 <a href="https://apisix.apache.org/zh/docs/apisix/plugin-develop/#%E6%8F%92%E4%BB%B6%E5%91%BD%E5%90%8D%EF%BC%8C%E4%BC%98%E5%85%88%E7%BA%A7%E5%92%8C%E5%85%B6%E4%BB%96">Apache APISIX 插件开发指南</a>。</li> </ul> <ol> <li>下面我们在 <code>apisix/plugins/</code> 目录下创建 <code>file-logger.lua</code> 文件。</li> <li>然后根据官方给出的 <code>example-plugin</code> 的例子做参考,来完成一个初始化版本。</li> </ol> <pre><code class="language-lua">-- 在头部引入我们所需要的模块 local log_util = require("apisix.utils.log-util") local core = require("apisix.core") local plugin = require("apisix.plugin") local ngx = ngx -- 声明插件名称 local plugin_name = "file-logger" -- 定义插件 schema 格式 local schema = { type = "object", properties = { path = { type = "string" }, }, required = {"path"} } -- 插件元数据 schema local metadata_schema = { type = "object", properties = { log_format = log_util.metadata_schema_log_format } } local _M = { version = 0.1, priority = 399, name = plugin_name, schema = schema, metadata_schema = metadata_schema } -- 检查插件配置是否正确 function _M.check_schema(conf, schema_type) if schema_type == core.schema.TYPE_METADATA then return core.schema.check(metadata_schema, conf) end return core.schema.check(schema, conf) end -- 日志阶段 function _M.log(conf, ctx) core.log.warn("conf: ", core.json.encode(conf)) core.log.warn("ctx: ", core.json.encode(ctx, true)) end return _M </code></pre> <p>通过 <code>example-plugin</code> 的例子完成了一个最小的可用插件文件后,便可通过 <code>core.log.warn(core.json.encode(conf))</code> 和 <code>core.log.warn("ctx: ", core.json.encode(ctx, true))</code> 将插件的配置数据和请求相关的数据信息输出到 <code>error.log</code> 文件中去。</p> <h3>启用插件并测试</h3> <p>下面通过创建一条测试路由,来测试插件是否能成功将我们为其配置的插件数据和请求相关的数据信息,打印到错误日志文件中去。</p> <ol> <li> <p>在本地准备一个测试上游(本文中使用的测试上游是我在本地创建的 <code>127.0.0.1:3030/api/hello</code>)。</p> </li> <li> <p>通过 <code>curl</code> 命令创建一条路由并启用我们新增的插件。</p> <pre><code class="language-shell"> curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { "file-logger": { "path": "logs/file.log" } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:3030": 1 } }, "uri": "/api/hello" }' </code></pre> <p>接着就会看到一个 <code>200</code> 的状态码,表明已成功创建了路由。</p> </li> <li> <p>运行 <code>curl</code> 命令向该路由发送请求,测试 <code>file-logger</code> 插件是否已经启动。</p> <pre><code class="language-shell">curl -i http://127.0.0.1:9080/api/hello HTTP/1.1 200 OK ... hello, world </code></pre> </li> <li> <p>在 <code>logs/error.log</code> 文件中会有一条这样的记录:</p> <p><img src="https://static.apiseven.com/202108/1644996952020-7a79d5df-e679-42f1-94f3-40a913db790c.png" alt="logs/error.log 记录" referrerpolicy="no-referrer"></p> <p>可以看到, 在 <code>conf</code> 参数中我们为插件配置的 <code>path: logs/file.log</code> 已经成功地保存了。到此我们已经成功创建了一个最小可用的插件,在日志阶段打印了 <code>conf</code> 和 <code>ctx</code> 参数的数据。</p> <p>之后,我们可以直接在该 <code>file-logger.lua</code> 插件代码文件中,为它编写核心功能。这里我们可以直接运行 <code>apisix reload</code> 命令来重新加载最新的插件代码,而无需重启 Apache APISIX。</p> </li> </ol> <h3>为 file-logger 插件编写核心功能</h3> <p><code>file-logger</code> 插件的主要功能是写日志数据。在经过询问和查阅资料后,我了解到 <a href="https://www.tutorialspoint.com/lua/lua_file_io.htm">Lua 的 IO 库</a>,于是确认了该插件的功能逻辑大致为以下几步:</p> <ol> <li> <p>每次接受请求之后,将日志数据输出到插件配置的 <code>path</code> 中去。</p> <ol> <li>首先,在日志阶段通过 <code>conf</code> 拿到 <code>file-logger</code> 中 <code>path</code> 的值。</li> <li>然后,通过 Lua IO 库来完成文件「创建」、「打开」、「写」、「刷新缓存」、「关闭」的操作。</li> </ol> </li> <li> <p>处理「文件打开」失败、「文件创建」失败等错误。</p> <pre><code class="language-lua"> local function write_file_data(conf, log_message) local msg, err = core.json.encode(log_message) if err then return core.log.error("message json serialization failed, error info : ", err) end local file, err = io_open(conf.path, 'a+') if not file then core.log.error("failed to open file: ", conf.path, ", error info: ", err) else local ok, err = file:write(msg, '\n') if not ok then core.log.error("failed to write file: ", conf.path, ", error info: ", err) else file:flush() end file:close() end end </code></pre> </li> <li> <p>参考 <code>http-logger</code> 插件源码,完成了将日记数据传给写日志数据的方法和 metadata 的一些判断和处理。</p> <pre><code class="language-lua"> function _M.log(conf, ctx) local metadata = plugin.plugin_metadata(plugin_name) local entry if metadata and metadata.value.log_format and core.table.nkeys(metadata.value.log_format) &gt; 0 then entry = log_util.get_custom_format_log(ctx, metadata.value.log_format) else entry = log_util.get_full_log(ngx, conf) end write_file_data(conf, entry) end </code></pre> </li> </ol> <h2>验证及添加测试</h2> <h3>验证收集日志记录</h3> <p>因为在创建测试路由时已经启用了 <code>file-logger</code> 插件,并且配置了 <code>path</code> 为 <code>logs/file.log</code>,所以此时我们只需给测试路由发送请求,以验证日志收集的结果:</p> <pre><code class="language-shell">curl -i http://127.0.0.1:9080/api/hello </code></pre> <p>在对应的 <code>logs/file.log</code> 中我们可以看到每条记录都是以 JSON 的格式保存下来。将其中一条数据格式化之后如下所示:</p> <pre><code class="language-json">{ "server":{ "hostname":"....", "version":"2.11.0" }, "client_ip":"127.0.0.1", "upstream":"127.0.0.1:3030", "route_id":"1", "start_time":1641285122961, "latency":13.999938964844, "response":{ "status":200, "size":252, "headers":{ "server":"APISIX\/2.11.0", "content-type":"application\/json; charset=utf-8", "date":"Tue, 04 Jan 2022 08:32:02 GMT", "vary":"Accept-Encoding", "content-length":"19", "connection":"close", "etag":"\"13-5j0ZZR0tI549fSRsYxl8c9vAU78\"" } }, "service_id":"", "request":{ "querystring":{ }, "size":87, "method":"GET", "headers":{ "host":"127.0.0.1:9080", "accept":"*\/*", "user-agent":"curl\/7.77.0" }, "url":"http:\/\/127.0.0.1:9080\/api\/hello", "uri":"\/api\/hello" } } </code></pre> <p>至此验证收集日志记录的环节结束了,验证结果说明插件成功启动并返回了应有的数据。</p> <h3>为插件添加测试</h3> <p>对于 <code>add_block_preprocessor</code> 部分的代码,由于我没有学过 Perl ,所以在刚开始编写时比较困惑。在询问之后才了解到它的正确使用方式:如果我们在数据部分没有编写相关的 <code>request</code> 断言和 <code>no_error_log</code> 断言时,那么默认断言如下:</p> <pre><code class="language-lua">--- request GET /t --- no_error_log [error] </code></pre> <p>综合参考了一些其他的日志测试文件以后,我在 <code>t/plugin/</code> 目录下创建 <code>file-logger.t</code> 文件。</p> <p>每一份测试文件都由 <code>__DATA__</code> 分为序言部分和数据部分。由于目前官网测试相关文档没有明确归档分类,更多具体内容可参考文末的相关资料。下面我为大家列出参考相关资料后完成的其中一个测试用例:</p> <pre><code class="language-perl">use t::APISIX 'no_plan'; no_long_string(); no_root_location(); add_block_preprocessor(sub { my ($block) = @_; if (! $block-&gt;request) { $block-&gt;set_value("request", "GET /t"); } if (! $block-&gt;no_error_log &amp;&amp; ! $block-&gt;error_log) { $block-&gt;set_value("no_error_log", "[error]"); } }); run_tests; __DATA__ === TEST 1: sanity --- config location /t { content_by_lua_block { local configs = { -- full configuration { path = "file.log" }, -- property "path" is required { path = nil } } local plugin = require("apisix.plugins.file-logger") for i = 1, #configs do ok, err = plugin.check_schema(configs[i]) if err then ngx.say(err) else ngx.say("done") end end } } --- response_body_like done property "path" is required </code></pre> <p>至此插件添加测试环节结束。</p> <h2>总结</h2> <p>以上就是我作为一个后端新手,从 0 开始实现一款 Apache APISIX 插件的全过程。在开发插件的过程中确实碰到了很多坑,比较幸运的是 Apache APISIX 社区里面有很多热心的大佬帮我解惑,使得 <code>file-logger</code> 插件的开发和测试全程都比较顺畅。如果你对这个插件感兴趣,或想要查看插件详情,可以参考 <a href="https://apisix.apache.org/zh/docs/apisix/next/plugins/file-logger/">Apache APISIX 官方文档</a>。</p> <p>目前,Apache APISIX 也在开发其他插件以支持集成更多服务,如果您对此感兴趣,欢迎随时在 <a href="https://github.com/apache/apisix/discussions">GitHub Discussion</a> 中发起讨论,也可通过<a href="https://apisix.apache.org/docs/general/subscribe-guide">邮件列表</a>进行交流讨论。</p> <h2>参考资料</h2> <ul> <li><a href="https://apisix.apache.org/zh/docs/apisix/plugin-develop/">Apache APISIX 插件开发指南</a></li> <li><a href="https://www.tutorialspoint.com/lua/lua_file_io.htm">Lua - File I/O 使用指南</a></li> <li><a href="https://apisix.apache.org/zh/docs/apisix/how-to-build/#%E6%AD%A5%E9%AA%A44%EF%BC%9A%E8%BF%90%E8%A1%8C%E6%B5%8B%E8%AF%95%E6%A1%88%E4%BE%8B">如何运行 Apache APISIX 测试案例</a></li> <li><a href="https://apisix.apache.org/zh/docs/apisix/plugin-develop/#%E7%BC%96%E5%86%99%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B">如何编写测试用例</a></li> <li><a href="https://apisix.apache.org/zh/docs/apisix/internal/testing-framework/">Apache APISIX 测试框架介绍</a></li> <li><a href="https://metacpan.org/pod/Test%3A%3ANginx%3A%3ASocket">test-nginx 相关的一些 API 介绍</a></li> </ul>

生态扩大进行中!Apache APISIX 集成 Splunk HTTP Event Collector

<p>随着技术的不断迭代和企业架构的不断演进,系统的复杂度越来越高。**日志作为分析和观测的“原材料”,能支持和兼容不同的分析引擎会为用户在选型和后期运维过程中降低很大成本。**基于日志的分析和观测作为保障系统稳定的基石,它的角色非常重要。</p> <p>Apache APISIX 作为一个高性能的 API 网关不仅在性能上有着良好的表现,并且在数据和日志的运维上通过和社区用户的交流共建也已经支持了大部分主流的开源及商业日志解决方案,包括:<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/http-logger.md">HTTP Logger</a> 、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/tcp-logger.md">TCP Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/kafka-logger.md">Kafka Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/udp-logger.md">UDP Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/rocketmq-logger.md">RocketMQ Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/skywalking-logger.md">SkyWalking Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/sls-logger.md">Aliyun Cloud Logging(SLS)</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/google-cloud-logging.md">Google Cloud Logging</a> 等。</p> <p>最近通过社区的共建支持,Apache APISIX 的 Logger 全家桶中又多了一位新成员:<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/google-cloud-logging.md">Splunk HEC Logging</a>。</p> <p>本文将为大家介绍如何在 Apache APISIX 中配置和使用 <a href="https://docs.splunk.com/Documentation/Splunk/8.2.3/Data/TroubleshootHTTPEventCollector">Splunk HEC</a> 服务。</p> <h2>关于 Splunk HTTP Event Collector</h2> <p><a href="https://www.splunk.com/">Splunk</a> 是一个机器数据的全文搜索引擎,可应用于采集、索引、搜索和分析各种应用数据,根据 <a href="https://db-engines.com/en/ranking/search+engine">DB Engines 的检索引擎排名</a>,目前 Splunk 位列第二,是一款应用广泛的全文检索软件。Splunk 和 ElasticSearch 一样,是准实时可以提供不间断搜索结果的数据流。</p> <p><a href="https://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector">Splunk HTTP Event Collector (HEC)</a> 是 Splunk 提供的 HTTP 事件收集器,主要提供以 HTTP(S) 协议将数据和应用程序事件发送到 Splunk 的能力。</p> <h2>关于 Splunk HEC Logging 插件</h2> <p><a href="https://github.com/apache/apisix/blob/master/docs/zh/latest/plugins/splunk-hec-logging.md">splunk-hec-logging</a> 插件用于将 Apache APISIX 的请求日志转发到 Splunk 中进行分析和存储。启用该插件后,Apache APISIX 将在 Log 阶段获取请求上下文信息,并将其序列化为 <a href="https://docs.splunk.com/Documentation/Splunk/latest/Data/FormateventsforHTTPEventCollector#Event_metadata">Splunk Event Data 格式</a>后提交到批处理队列中。当触发批处理队列每批次最大处理容量,或刷新缓冲区的最大时间时,会将队列中的数据提交到 Splunk HEC 中。</p> <h2>如何使用 Splunk HEC Logging 插件</h2> <h3>Splunk 配置步骤</h3> <h4>部署 Splunk Enterprise</h4> <p>请参考 Splunk 的 <a href="https://docs.splunk.com/Documentation/Splunk/8.2.3/Installation/Chooseyourplatform">官方安装指南</a> 进行部署,本文将通过 Docker 进行部署演示。 Docker 命令参数如下:</p> <pre><code class="language-shell">docker run -p 18088:8088 -p 18000:8000 \ # 8088为HEC端口,8000为管理后台端口 -e "SPLUNK_PASSWORD=your-password" \ # 管理后台登录密码 -e "SPLUNK_START_ARGS=--accept-license" \ # 接受许可证条款(Splunk默认将提供一张Enterprise Trial License) -e "SPLUNK_HEC_TOKEN=your-hec-token" \ # 设置默认HEC令牌,配置此项后将创建一个默认的HEC -itd --rm --name splunk-example splunk/splunk:latest </code></pre> <p>命令参数具体释义可参考:<a href="https://splunk.github.io/docker-splunk/">Docker Splunk 文档</a>。</p> <h4>配置 Splunk HEC</h4> <p>Docker 中已经配置并创建了默认的 HEC,在这里不再过多赘述创建 HEC 的流程。具体手动创建的流程可参考文档:<a href="https://docs.splunk.com/Documentation/Splunk/8.2.3/Data/UsetheHTTPEventCollector">Set up and use HTTP Event Collector in Splunk Web</a>。</p> <h4>登录 Splunk Enterprise 并检查 HEC</h4> <p>通过浏览器访问 Docker 的映射端口。因为需要把管理后台的 <code>8000</code> 端口映射到宿主机的 <code>18000</code> 端口,所以在操作时可以在宿主机上通过「回环地址加端口」的方式在浏览器访问即可。例如:http://127.0.0.1:18000,登录的默认用户名是 admin,密码是在上例的环境变量中设置的 <code>SPLUNK_PASSWORD</code> 的值。</p> <p>如下图所示,表示登录成功。</p> <p><img src="https://static.apiseven.com/202108/1644488346684-867cb3a1-a6fd-4dc6-8cb7-f08c1843ac66.png" alt="Splunk 登录界面" referrerpolicy="no-referrer"></p> <p>单击界面右上方 “Settings &gt; Data Inputs” 检查默认 HEC 是否设置成功:</p> <p><img src="https://static.apiseven.com/202108/1644488375914-ce04f987-eb8e-4708-93b7-f0685b145fff.png" alt="默认 HEC" referrerpolicy="no-referrer"></p> <p>在 HTTP Event Collector 的 Inputs 列中我们已经可以看到 HEC 的数量,表示设置成功。</p> <p><img src="https://static.apiseven.com/202108/1644488402376-09c44d02-c6aa-4948-8ca7-2b58a310d010.png" alt="HEC 的数量" referrerpolicy="no-referrer"></p> <p>此时可以点击 HTTP Event Collector 进入 HEC 详情列表查看 HECs 的 Token 信息。</p> <p><img src="https://static.apiseven.com/202108/1644488428695-db33a594-df06-4857-9421-60bd30cae91e.png" alt="HECs 的 Token 信息" referrerpolicy="no-referrer"></p> <p>Token Values 即在上文中 Docker 环境变量中配置的 <code>SPLUNK_HEC_TOKEN</code> 的值。</p> <h3>Apache APISIX 配置步骤</h3> <h4>启用插件</h4> <p>运行以下命令,启用 <code>splunk-hec-logging</code> 插件。</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins":{ "splunk-hec-logging":{ "endpoint":{ // HEC 端点地址 "uri":"http://127.0.0.1:18088/services/collector", // HEC Token "token":"BD274822-96AA-4DA6-90EC-18940FB2414C" }, // // 刷新批处理队列缓冲区的最大时间(以秒为单位) "inactive_timeout":2, // 每个批处理队列最大容纳日志条目数 "batch_max_size":10 } }, "upstream":{ "type":"roundrobin", "nodes":{ "127.0.0.1:1980":1 } }, "uri":"/splunk.do" }' </code></pre> <p>插件参数说明如下表所示。</p> <table> <thead> <tr> <th>名称</th> <th>是否必填</th> <th>默认值</th> <th>描述</th> </tr> </thead> <tbody> <tr> <td>endpoint</td> <td>是</td> <td>N/A</td> <td>Splunk HEC 端点配置信息</td> </tr> <tr> <td>endpoint.uri</td> <td>是</td> <td>N/A</td> <td>Splunk HEC 事件收集API</td> </tr> <tr> <td>endpoint.token</td> <td>是</td> <td>N/A</td> <td>Splunk HEC 身份令牌</td> </tr> <tr> <td>endpoint.channel</td> <td>否</td> <td>N/A</td> <td>Splunk HEC 发送渠道标识,参考:<a href="https://docs.splunk.com/Documentation/Splunk/8.2.3/Data/AboutHECIDXAck">About HTTP Event Collector Indexer Acknowledgment</a></td> </tr> <tr> <td>endpoint.timeout</td> <td>否</td> <td>10</td> <td>Splunk HEC 数据提交超时时间(以秒为单位)</td> </tr> <tr> <td>ssl_verify</td> <td>否</td> <td>TRUE</td> <td>启用 SSL 验证, 参考:<a href="https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake">OpenResty 文档</a></td> </tr> <tr> <td>max_retry_count</td> <td>否</td> <td>0</td> <td>从处理管道中移除之前的最大重试次数</td> </tr> <tr> <td>retry_delay</td> <td>否</td> <td>1</td> <td>如果执行失败,流程执行应延迟的秒数</td> </tr> <tr> <td>buffer_duration</td> <td>否</td> <td>60</td> <td>必须先处理批次中最旧条目的最大期限(以秒为单位)</td> </tr> <tr> <td>inactive_timeout</td> <td>否</td> <td>5</td> <td>刷新缓冲区的最大时间(以秒为单位)</td> </tr> <tr> <td>batch_max_size</td> <td>否</td> <td>1000</td> <td>每个批处理队列可容纳的最大条目数</td> </tr> </tbody> </table> <h4>发送请求</h4> <p>运行以下命令,向 Splunk 发送请求。</p> <pre><code class="language-shell">$ curl -i http://127.0.0.1:9080/splink.do HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Date: Fri, 10 Dec 2021 09:57:52 GMT Server: APISIX/2.11.0 Hello, Splunk HEC Logging </code></pre> <h4>验证日志</h4> <p>登录 Splunk 控制台,点击 “Search &amp; Reporting”。</p> <p><img src="https://static.apiseven.com/202108/1644487376250-7c5d32b0-368a-4ee2-9285-38cb0ee571f0.png" alt="Splunk 控制台" referrerpolicy="no-referrer"></p> <p>在搜索输入框中输入:<code>source="apache-apisix-splunk-hec-logging"</code>,即可查询到发送的请求日志。</p> <p><img src="https://static.apiseven.com/202108/1644487401080-b3b18cf9-f576-4e05-be98-2d5fde34fe8e.png" alt="查询请求日志" referrerpolicy="no-referrer"></p> <h4>停用插件</h4> <p>移除 <code>splunk-hec-logging</code> 相关配置即可。</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri":"/logging.do", "upstream":{ "type":"roundrobin", "nodes":{ "127.0.0.1:1980":1 } }, "plugins":{ } }' </code></pre> <h2>总结</h2> <p>目前,Apache APISIX 也在开发其他插件以支持集成更多服务,如果您对此感兴趣,欢迎随时在 <a href="https://github.com/apache/apisix/discussions">GitHub Discussion</a> 中发起讨论,也可通过<a href="https://apisix.apache.org/zh/docs/general/subscribe-guide">邮件列表</a>进行交流。</p> <h2>相关阅读</h2> <ul> <li><a href="https://apisix.apache.org/zh/blog/2022/01/17/apisix-kafka-integration">Apache APISIX 集成 Kafka 实现高效率实时日志监控</a></li> <li><a href="https://apisix.apache.org/zh/blog/2021/12/08/apisix-integrate-rocketmq-logger-plugin">Apache APISIX 携手 RocketMQ 为实时 API 日志监控功能再下一城</a></li> <li><a href="https://apisix.apache.org/zh/blog/2021/12/22/google-logging">捷报频传!Apache APISIX 现已支持对接 Google Cloud Logging</a></li> <li><a href="https://apisix.apache.org/zh/blog/2021/12/07/apisix-integrate-skywalking-plugin">强强联合!APISIX 集成 SkyWalking 打造全方位日志处理</a></li> </ul>

新插件 forward-auth 已上线,认证功能又多一项选择

<p>Forward Auth 能巧妙地将认证与授权逻辑转移至专门的外部服务中,网关会将用户的请求转发至认证服务中,并在认证服务响应非 20x 状态时,阻止原有请求并替换结果。通过这样的方式,就可以实现认证未通过时,返回自定义报错或用户重定向至认证页面。接下来将为大家介绍 Apache APISIX 中新增插件 <code>forward-auth</code> 的使用方法。</p> <h2>原理</h2> <p><img src="https://static.apiseven.com/202108/1643096414141-ccbc33c0-7899-445a-a2f8-b6d5341c44df.jpg" alt="插件原理图" referrerpolicy="no-referrer"></p> <p>关于 <code>forward-auth</code> 插件在 Apache APISIX 中的运行原理与流程如上图所示,具体总结为以下几步:</p> <ul> <li>第一步:由客户端向 APISIX 发起请求</li> <li>第二步:由 APISIX 向用户配置的认证服务发起请求</li> <li>第三步:认证服务响应(2xx 或异常状态)</li> <li>第四步:APISIX 会根据认证服务响应,决定向上游转发请求或直接向客户端发送拒绝响应</li> </ul> <h2>如何使用</h2> <h3>步骤一:设置认证服务</h3> <p>假设有这样一项认证服务,用户向其发送带有 Authorization 请求头的请求。如果这个数据通过验证则返回 200 状态码和一个名为 <code>X-User-ID</code> 的响应头;如果没有通过验证则认为认证状态过期,返回 302 状态码和 <code>Location</code> 响应头将客户端重定向至登录页面。</p> <h3>步骤二:创建路由并开启 <code>forward-auth</code> 插件</h3> <p>接下来,我们将配置一个路由并开启 <code>forward-auth</code> 插件,将上述的认证服务和上游应用对接起来。</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { "forward-auth": { "address": "http://127.0.0.1:9080/auth", "request_headers": ["Authorization"], "upstream_headers": ["X-User-ID"], "client_headers": ["Location"] } }, "uri": "/user" }' </code></pre> <p>上述配置细节释义:</p> <ul> <li>当有请求匹配到当前路由时,发送一个请求至 <code>address</code> 中的地址,其中将附带<code>request_headers</code> 中定义的请求头 <code>Authorization</code>(即配置需要由客户端转发至认证服务的请求头,如果不设置则不转发任何请求头),认证服务可以据此进行用户身份确认。</li> <li>如果认证通过,状态码为 200 并返回一个 <code>upstream_headers</code> 中定义的 <code>X-User-ID</code>(即认证通过时由认证服务转发至上游的请求头,如果不设置则不转发任何请求头)。</li> <li>如果认证失败,状态码为 302 并返回一个在 <code>client_headers</code> 中定义的 <code>Location</code>(即认证失败时由认证服务向客户端发送的响应头,如果不设置则不转发任何响应头)。</li> </ul> <h3>步骤三:测试请求</h3> <pre><code class="language-shell"># 使用 POST 请求并发送数据 curl http://127.0.0.1:9080/user \ --header 'Authorization: true' HTTP/1.1 200 OK Content-Type: application/json Content-Length: 28 Server: APISIX/2.11.0 {"user_id":"i-am-real-user"} # 使用 GET 请求 curl -i http://127.0.0.1:9080/user \ --header 'Authorization: false' HTTP/1.1 302 FOUND Server: APISIX/2.11.0 Location: https://example.com/auth </code></pre> <h3>补充:关闭插件</h3> <p>如已使用完毕,只需移除路由配置中 <code>forward-auth</code> 插件相关配置并保存,即可关闭路由上的 Forward Auth 插件。得益于 Apache APISIX 的动态化特性,开启关闭插件的过程都不需要重启 Apache APISIX。</p> <h2>总结</h2> <p>想要获取更多关于 <code>forward-auth</code> 插件说明和完整配置列表,可参考<a href="https://apisix.apache.org/docs/apisix/next/plugins/forward-auth">官方文档</a>。同时,如果您有更加复杂的认证或授权应用场景,也可以尝试使用 <code>opa</code> 插件,该插件允许以可编程的方式提供更强大的功能。</p> <p>目前,Apache APISIX 也在开发其他插件以支持集成更多服务,如果您对此感兴趣,欢迎随时在 <a href="https://github.com/apache/apisix/discussions">GitHub Discussion</a> 中发起讨论,也可通过<a href="https://apisix.apache.org/zh/docs/general/subscribe-guide">邮件列表</a>进行交流。</p>

Apache APISIX 新技能,代理 gRPC-Web 请求

<h2>gRPC Web 背景介绍</h2> <p>gRPC 最初由谷歌开发,是一个基于 HTTP/2 实现的高性能远程过程调用框架。但由于浏览器没有直接暴露 HTTP/2,所以 Web 应用程序不能直接使用 gRPC。gRPC Web 是一个标准化协议,它解决了这个问题。</p> <p>第一个 gRPC-web 实现是在 2018 年作为一个 JavaScript 库发布的,Web 应用程序可以通过它直接与 gRPC 服务通信。其原理是创建与 HTTP/1.1 和 HTTP/2 兼容的端到端 gRPC 管道,然后浏览器发送常规的 HTTP 请求,位于浏览器和服务器之间的 gRPC-Web 代理对请求和响应进行转换。与 gRPC 类似,gRPC Web 在 Web 客户端和后端 gRPC 服务之间使用预定义的契约。Protocol Buffers 被用来序列化和编码消息。</p> <p><img src="https://static.apiseven.com/202108/1643099754071-1f5c3c68-f2bc-4746-95f5-cc083ace554b.png" alt="gRPC-web 工作原理" referrerpolicy="no-referrer"></p> <p>有了 gRPC Web,用户可以使用浏览器或 Node 客户端直接调用后端的 gRPC 应用程序。不过,在浏览器端使用 gRPC-Web 调用 gRPC 服务也存在一些限制:</p> <ul> <li>不支持客户端流和双向流调用。</li> <li>跨域调用 gRPC 服务需要在服务器端配置 CORS。</li> <li>gRPC 服务器端必须配置为支持 gRPC-Web,或者必须有第三方服务代理在浏览器和服务器之间对调用进行转换。</li> </ul> <h2>Apache APISIX gRPC Web Proxy</h2> <p>Apache APISIX 通过插件的方式支持 gRPC Web 协议的代理,在 <code>grpc-web</code> 插件中完成了 gRPC Web与 gRPC Server 通讯时的协议转换及数据编解码工作,其通讯的过程如下:</p> <p>gRPC Web Client -&gt; Apache APISIX(protocol conversion &amp; data codec) -&gt; gRPC server</p> <p>接下来通过一个完整的示例向大家演示怎样构建一个 gRPC Web 客户端,并通过 Apache APISIX 进行 gRPC Web 请求的代理。在以下的示例中,我们会将 Go 作为 gRPC Server 服务端处理程序,Node 作为 gRPC Web 客户端请求程序。</p> <h2>配置 Protocol Buffer</h2> <p>首先进行第一步,安装 Protocol Buffer 编译器及相关插件。</p> <ol> <li> <p>安装 <code>protoc</code> 和 <code>proto-grpc-*</code> 插件。</p> <p>在编写客户端和服务端程序前,需要在系统中安装 Protocol Buffer 编译器 <code>protoc</code> 和 用于生成 <code>.proto</code> 的 Go、JavaScript、gRPC Web 接口代码的 <code>protoc-gen-go</code> 和 <code>protoc-gen-grpc-web</code> 插件。</p> <p>请运行以下脚本,安装上述组件。</p> <pre><code class="language-bash">#!/usr/bin/env bash set -ex PROTOBUF_VERSION="3.19.0" wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip unzip protoc-${PROTOBUF_VERSION}-linux-x86_64.zip mv bin/protoc /usr/local/bin/protoc mv include/google /usr/local/include/ chmod +x /usr/local/bin/protoc PROTO_GO_PLUGIN_VER="1.2.0" wget https://github.com/grpc/grpc-go/releases/download/cmd/protoc-gen-go-grpc/v${PROTO_GO_PLUGIN_VER}/protoc-gen-go-grpc.v${PROTO_GO_PLUGIN_VER}.linux.amd64.tar.gz tar -zxvf protoc-gen-go-grpc.v${PROTO_GO_PLUGIN_VER}.linux.amd64.tar.gz mv protoc-gen-go-grpc /usr/local/bin/protoc-gen-go chmod +x /usr/local/bin/protoc-gen-go PROTO_JS_PLUGIN_VER="1.3.0" wget https://github.com/grpc/grpc-web/releases/download/${PROTO_JS_PLUGIN_VER}/protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64 mv protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64 /usr/local/bin/protoc-gen-grpc-web chmod +x /usr/local/bin/protoc-gen-grpc-web </code></pre> </li> <li> <p>创建 <code>SayHello</code> 示例 <code>proto</code> 文件。</p> <pre><code class="language-go"> // a6/echo.proto syntax = "proto3"; package a6; option go_package = "./;a6"; message EchoRequest { string message = 1; } message EchoResponse { string message = 1; } service EchoService { rpc Echo(EchoRequest) returns (EchoResponse); } </code></pre> </li> </ol> <h2>配置服务端程序</h2> <ol> <li> <p>生成服务端 Go 原始消息和服务/客户端存根。</p> <pre><code class="language-bash">protoc -I./a6 echo.proto --go_out=plugins=grpc:./a6 </code></pre> </li> <li> <p>实现服务端处理程序接口。</p> <pre><code class="language-go">// a6/echo.impl.go package a6 import ( "errors" "golang.org/x/net/context" ) type EchoServiceImpl struct { } func (esi *EchoServiceImpl) Echo(ctx context.Context, in *EchoRequest) (*EchoResponse, error) { if len(in.Message) &lt;= 0 { return nil, errors.New("message invalid") } return &amp;EchoResponse{Message: "response: " + in.Message}, nil } </code></pre> </li> <li> <p>服务端程序运行入口文件。</p> <pre><code class="language-go">// server.go package main import ( "fmt" "log" "net" "apisix.apache.org/example/a6" "google.golang.org/grpc" ) func main() { lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 50001)) if err != nil { log.Fatalf("failed to listen: %v", err) } grpcServer := grpc.NewServer() a6.RegisterEchoServiceServer(grpcServer, &amp;a6.EchoServiceImpl{}) if err = grpcServer.Serve(lis); err != nil { log.Fatalf("failed to serve: %s", err) } } </code></pre> </li> <li> <p>编译并启动服务端服务。</p> <pre><code class="language-bash">go build -o grpc-server server.go ./grpc-server </code></pre> </li> </ol> <h2>配置客户端程序</h2> <ol> <li> <p>生成客户端 <code>proto</code> 代码</p> <p>生成客户端 JavaScript 原始消息、服务/客户端存根和 gRPC Web 的 JavaScript 的接口代码。 gRPC Web 的 <code>proto</code> 插件提供了两种代码生成模式:</p> <ol> <li>mode=grpcwebtext: 默认生成的代码以 grpc-web-text 格式发送 payload</li> </ol> <ul> <li>Content-type: application/grpc-web-text</li> <li>Payload 使用 base64 编码</li> <li>支持一元和服务器流式调用</li> </ul> <ol> <li>mode=grpcweb:以二进制 protobuf 格式发送 payload</li> </ol> <ul> <li>Content-type: application/grpc-web+proto</li> <li>Payload 采用二进制 protobuf 格式</li> <li>目前仅支持一元调用</li> </ul> </li> </ol> <pre><code class="language-bash">$ protoc -I=./a6 echo.proto --js_out=import_style=commonjs:./a6 --grpc-web_out=import_style=commonjs,mode=grpcweb:./a6 </code></pre> <ol start="2"> <li> <p>安装客户端依赖库</p> <pre><code class="language-shell">$ npm i grpc-web $ npm i google-protobuf </code></pre> </li> <li> <p>客户端执行入口文件</p> <pre><code class="language-shell">// client.js const {EchoRequest} = require('./a6/echo_pb'); const {EchoServiceClient} = require('./a6/echo_grpc_web_pb'); // 连接到 Apache APISIX 的入口 let echoService = new EchoServiceClient('http://127.0.0.1:9080'); let request = new EchoRequest(); request.setMessage("hello") echoService.echo(request, {}, function (err, response) { if (err) { console.log(err.code); console.log(err.message); } else { console.log(response.getMessage()); } }); </code></pre> </li> <li> <p>最终项目结构</p> <pre><code class="language-bash">$ tree . ├── a6 │ ├── echo.impl.go │ ├── echo.pb.go │ ├── echo.proto │ ├── echo_grpc_web_pb.js │ └── echo_pb.js ├── client.js ├── server.go ├── go.mod ├── go.sum ├── package.json └── package-lock.json </code></pre> </li> </ol> <p>完成上述的步骤之后,你已经配置了把 gRPC Server 的服务端程序和 gRPC Web 的客户端程序,并且启动了服务端程序,它将通过 <code>50001</code> 端口接收请求。</p> <h2>配置 Apache APISIX</h2> <p>接下来只需在 Apache APISIX 路由的插件配置中启用 <code>grpc-web</code> 插件,即可进行 gRPC Web 请求的代理。</p> <ol> <li> <p>启用 <code>grpc-web</code> 代理插件</p> <p>启用 <code>grpc-web</code> 代理插件,路由必须使用<strong>前缀匹配</strong>模式(例如:<code>/* 或 /grpc/example/*</code>), 因 gRPC Web 客户端会在 URI 中传递 <code>proto</code> 中声明的包名称、服务接口名称、方法名称等信息(例如:<code>/path/a6.EchoService/Echo</code>),<strong>使用绝对匹配时会使插件无法从 URI 中提取 <code>proto</code> 信息</strong>。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri":"/*", // 前缀匹配模式 "plugins":{ "grpc-web":{} //开启 gRPC Web 代理 }, "upstream":{ "scheme":"grpc", "type":"roundrobin", "nodes":{ "127.0.0.1:50001":1 // gRPC Server Listen 地址和端口 } } }' </code></pre> </li> <li> <p>验证 gRPC Web 代理请求</p> <p>通过 Node 执行 <code>client.js</code> 即可向 Apache APISIX 发送 gRPC Web 协议请求。 上述客户端和服务端的处理逻辑分别是:客户端向服务端发送一条消息内容是 <code>hello</code>,服务端收到消息后响应 <code>response: hello</code>,执行结果如下。</p> <pre><code class="language-shell">$ node client.js response: hello </code></pre> </li> <li> <p>关闭 <code>grpc-web</code> 代理插件</p> <p>只需将路由插件配置中的 grpc-web 属性移除即可。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri":"/*", "plugins":{ }, "upstream":{ "scheme":"grpc", "type":"roundrobin", "nodes":{ "127.0.0.1:50001":1 } } }' </code></pre> </li> </ol> <h2>总结</h2> <p>本文为大家带来了 Apache APISIX 的 <code>grpc-web</code> 插件讲解及实战案例。</p> <p>欢迎随时在 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 中发起讨论,或通过<a href="https://apisix.apache.org/zh/docs/general/subscribe-guide">邮件列表</a>进行交流。</p>

Apache APISIX 集成 HashiCorp Vault,生态系统再添一员

<p>随着微服务架构的兴起,保持服务安全变得比以前更有挑战性。多个后端 server 实例使用单一的静态密钥凭访问数据库 server 会带来巨大的风险。如果发生密钥凭证泄露,整个系统都会受到影响。为了解决密钥凭证泄露所带来的影响,只能撤销这个密钥凭证。而撤销密钥凭证会导致大规模的服务中断,对开发者来说,服务大规模中断是开发者最不想看到事情。</p> <p>虽然我们不能提前预知将来会出现哪些安全漏洞,但是我们可以通过配置多个密钥来控制这些安全漏洞的影响范围。为了避免这样的情况,像 HashiCorp Vault (下文简称 Vault)这样密钥凭证解决方案应运而生。</p> <p>本文演示了如何将 Vault 与 Apache APISIX 的 <code>jwt-auth</code> 插件集成,在为服务提供高并发低延迟的卓越性能的同时,为服务的安全保驾护航。</p> <h2>什么是 Vault</h2> <p>Vault 旨在帮助用户管理服务密钥的访问权限,并在多个服务之间安全地传输这些密钥。密钥可以是任何形式的凭证,因为密钥可用于解锁敏感信息,所以需要严密控制密钥。密钥的形式可以是密码、API 密钥、SSH 密钥、RSA 令牌或 OTP。事实上,密钥泄露的情况非常普遍:密钥通常被储存在配置文件中,或作为变量被储存在代码中,如果没有妥善保存,密钥甚至会出现在 GitHub、BitBucket 或 GitLab 等公开可见的代码库中,从而对安全构成了重大威胁。Vault 通过集中密钥解决了这个问题。它为静态密钥提供加密存储,生成具有 TTL 租约的动态密钥,对用户进行认证,以确保他们有权限访问特定的密钥。因此,即使在安全漏洞的情况下,影响范围也小得多,并能得到更好的控制。</p> <p>Vault 提供了一个用户界面用于密钥管理,使控制和管理权限变得非常容易。不仅如此,它还提供了灵活且详细审计日志功能,能跟踪到所有用户的历史访问记录。</p> <p><img src="https://static.apiseven.com/202108/1642770417379-a91960a5-5aac-45fa-9277-801a4ee2afc6.png" alt="HashiCorp Vault" referrerpolicy="no-referrer"></p> <h2>jwt-auth 插件介绍</h2> <p><code>jwt-auth</code> 是一个认证插件,可以附加到任何 APISIX 路由上,在请求被转发到上游 URI 之前执行 JWT 认证。通常情况下,发行者使用私钥或文本密钥来签署 JWT。JWT 的接收者将验证签名,以确保令牌在被发行者签名后没有被改变。整个 JWT 机制的整体完整性取决于签名密钥(或 RSA 密钥对的文本密钥)。这使得未经认证的来源很难猜到签名密钥并试图改变 JWT 中的声明。</p> <p>因此,在安全的环境中存储这些密钥是非常关键的。如果密钥落入坏人之手,可能会危及整个基础设施的安全。虽然 Apache APISIX 采取了一切手段来遵循标准的 SecOps 实践,但在生产环境中有一个集中的密钥管理解决方案也是一件好事。例如 Vault,有详细的审计日志,定时的密钥轮换,密钥撤销等功能。如果每次在整个基础设施发生密钥轮换时,你都要更新 Apache APISIX 配置,这将是一个相当麻烦的问题。</p> <h2>如何集成 Vault 和 Apache APISIX</h2> <p>为了与 Vault 集成,Apache APISIX 需要在 <a href="https://github.com/apache/apisix/blob/master/conf/config.yaml">config.yaml</a> 文件中加载 Vault 的相关配置信息。</p> <p>Apache APISIX 与 Vault server <a href="https://www.vaultproject.io/docs/secrets/kv/kv-v1">KV secret engine v1</a> HTTP <a href="https://www.vaultproject.io/api/secret/kv/kv-v1">APIs</a> 进行通信。由于大多数企业解决方案倾向于在生产环境中使用 KV Secrets Engine - Version 1,在 APISIX-Vault 支持的初始阶段,我们只使用这个版本。在以后的版本中,我们将增加对 K/V - Version 2 的支持。</p> <p>使用 Vault 而不是 APISIX etcd 后端的主要顾虑是在低信任度环境下的安全问题。因为 Vault 访问令牌是小范围的,可以授予 APISIX server 有限的权限。</p> <h3>配置 Vault</h3> <p>本节分享了在 Apache APISIX 生态系统中使用 Vault 的最佳实践。如果你已经有了一个具有必要权限的 Vault 实例在运行,请跳过本节。</p> <h4>启动 Vault server</h4> <p>在这里,你有多种选择,可以自由选择 Docker、预编译二进制包或从源代码构建。至于与 Vault server 的通信,你需要一个 Vault CLI 客户端。请运行以下命令启动 server:</p> <pre><code class="language-shell">$ vault server -dev -dev-root-token-id=root … WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory and starts unsealed with a single unseal key. The root token is already authenticated to the CLI, so you can immediately begin using Vault. You may need to set the following environment variable: export VAULT_ADDR='http://127.0.0.1:8200' The unseal key and root token are displayed below in case you want to seal/unseal the Vault or re-authenticate. Unseal Key: 12hURx2eDPKK1tzK+8TkgH9pPhPNJFpyfc/imCLgJKY= Root Token: root Development mode should NOT be used in production installations! </code></pre> <p>用正确的环境变量设置 Vault CLI 客户端。</p> <pre><code class="language-shell">$ export VAULT_ADDR='http://127.0.0.1:8200' $ export VAULT_TOKEN='root' </code></pre> <p>用一个合适的 <code>path</code> 前缀启用 vault k/v version 1的密钥引擎后端。在这个演示中,我们要选择 <code>kv</code> 路径,这样就不会与 vault 默认的 <code>kv</code> version 2 的密钥路径发生冲突。</p> <pre><code class="language-shell">$ vault secrets enable -path=kv -version=1 kv Success! Enabled the kv secrets engine at: kv/ # To reconfirm the status, run $ vault secrets list Path Type Accessor Description ---- ---- -------- ----------- cubbyhole/ cubbyhole cubbyhole_4eeb394c per-token private secret storage identity/ identity identity_5ca6201e identity store kv/ kv kv_92cd6d37 n/a secret/ kv kv_6dd46a53 key/value secret storage sys/ system system_2045ddb1 system endpoints used for control, policy and debugging </code></pre> <h4>为 Apache APISIX 生成一个 Vault 访问令牌</h4> <p>本文是关于在 <code>jwt-auth</code> 插件中使用 Vault 的介绍。因此,对于一个 APISIX Consumer <code>jack</code>,<code>jwt-auth</code> 插件会在 <code>&lt;vault.prefix inside config.yaml&gt;/consumer/&lt;consumer.username&gt;/jwt-auth</code> 中查找(如果启用了 Vault 配置)<code>secret/s</code> 到 Vault 键值对 存储。在这种情况下,如果你将 <code>kv/apisix</code> 命名空间(Vault 路径)指定为<code>config.yaml</code> 内的 <code>vault.prefix</code>,用于所有 APISIX 相关数据的检索,我们建议你为路径 <code>kv/apisix/consumer/</code> 创建一个策略。最后的星号(*)确保策略允许读取任何具有 <code>kv/apisix/consumer</code> 前缀的路径。</p> <p>用 HashiCorp 配置语言(HCL)创建一个策略文件。</p> <pre><code class="language-shell">$ tee apisix-policy.hcl &lt;&lt; EOF path "kv/apisix/consumer/*" { capabilities = ["read"] } EOF </code></pre> <p>将策略应用于 Vault 实例。</p> <pre><code class="language-shell">$ vault policy write apisix-policy apisix-policy.hcl Success! Uploaded policy: apisix-policy </code></pre> <p>用新定义的策略生成一个令牌,该策略已被配置为很小的访问边界。</p> <pre><code class="language-shell">$ vault token create -policy="apisix-policy" Key Value --- ----- token s.KUWFVhIXgoRuQbbp3j1eMVGa token_accessor nPXT3q0mfZkLmhshfioOyx8L token_duration 768h token_renewable true token_policies ["apisix-policy" "default"] identity_policies [] policies ["apisix-policy" "default"] </code></pre> <p>在这个例子中,<code>s.KUWFVhIXgoRuQbbp3j1eMVGa</code> 是你的访问令牌。</p> <h3>在 Apache APISIX 中添加 Vault 配置</h3> <p>Apache APISIX 通过 Vault HTTP APIs 与 Vault 实例进行通信。必要的配置必须被添加到 <a href="https://github.com/apache/apisix/blob/master/conf/config.yaml">config.yaml</a> 中。</p> <p>下面是关于你可以使用的不同字段的简要信息。</p> <ul> <li>host: 运行 Vault server 的主机地址。</li> <li>timeout: 每次请求的 HTTP 超时。</li> <li>token: 从 Vault 实例生成的令牌,授予从 Vault 读取数据的权限。</li> <li>prefix:启用前缀可以更好地执行策略,生成有限范围的令牌,严格控制可以从 APISIX 访问的数据。有效的前缀是(<code>kv/apisix</code>、<code>secret</code>等)。</li> </ul> <pre><code class="language-shell">vault: host: 'http://0.0.0.0:8200' timeout: 10 token: 's.KUWFVhIXgoRuQbbp3j1eMVGa' prefix: 'kv/apisix' </code></pre> <h3>创建一个 APISIX Consumer</h3> <p>APISIX 有一个 Consumer 层面的抽象,与认证方案并列。为了启用任何 APISIX 路由的认证,需要一个具有适合该特定类型认证服务配置的 Consumer。之后将通过 APISIX 成功执行 Consumer 配置方面的认证请求转发到上游 URI。APISIX Consumer 有两个字段:一个是 <code>username</code>(必填项),用于识别 Consumer;另一个是 <code>plugins</code>,用于保存 Consumer 所使用的特定插件配置。</p> <p>在这里,在这篇文章中,我们将用 <code>jwt-auth</code> 插件创建一个 Consumer。它为各自的路由或服务执行 JWT 认证。</p> <p>运行以下命令,启用 Vault 配置的 <code>jwt-auth</code>。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "jack", "plugins": { "jwt-auth": { "key": "test-key", "vault": {} } } }' </code></pre> <p>在这里,插件在 Consumer 配置中提到的 Consumer <code>jack</code> 的 Vault 路径(<code>&lt;vault.prefix from conf.yaml&gt;/consumer/jack/jwt-auth</code>)中查找密钥 <code>secret</code>,并使用它进行后续的签名和 jwt 验证。如果在同一路径中没有找到密钥,该插件会记录错误,并且无法执行 JWT 验证。</p> <h4>设置一个测试的上游 server</h4> <p>为了测试这个行为,你可以为一个上游创建一个路由(一个简单的 ping 处理程序,返回 pong)。你可以用一个普通的 go HTTP-Server 来设置它。</p> <pre><code class="language-go">// simple upstream server package main import "net/http" func ping(w http.ResponseWriter, req *http.Request) { w.Write([]byte("secure/pong\n")) } func main() { http.HandleFunc("/secure/ping", ping) http.ListenAndServe(":9999", nil) } </code></pre> <h4>创建一个启用了认证的 APISIX 路由</h4> <p>用这个安全的 ping HTTP server 和启用了 <code>jwt-auth</code> 认证插件创建一个 APISIX 路由。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { "jwt-auth": {} }, "upstream": { "nodes": { "127.0.0.1:9999": 1 }, "type": "roundrobin" }, "uri": "/secure/ping" }' </code></pre> <h4>从 jwt-auth 插件生成令牌</h4> <p>现在从 APISIX 签署一个 JWT 密文,可以用于并通过向 APISIX server 的 <code>http://localhost:9080/secure/ping</code> 代理路由发出请求。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/plugin/jwt/sign\?key\=test-key -i HTTP/1.1 200 OK Date: Tue, 18 Jan 2022 07:50:57 GMT Content-Type: text/plain; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Server: APISIX/2.11.0 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ0ZXN0LWtleSIsImV4cCI6MTY0MjU3ODY1N30.nkyev1_KUapVgY_QVYETsSApA6gEkDWS8tsHFV1EpD8 </code></pre> <p>在上一步中,如果你看到类似 <code>failed to sign jwt</code> 的信息,请确保你有一个私有密钥存储在 vault <code>kv/apisix/consumers/jack/jwt-aut</code> 路径中。</p> <pre><code class="language-shell"># example $ vault kv put kv/apisix/consumer/jack/jwt-auth secret=$ecr3t-c0d3 Success! Data written to: kv/apisix/consumer/jack/jwt-auth </code></pre> <h4>向 APISIX Server 发送请求</h4> <p>现在,向 APISIX 代理发出一个路由 <code>/secure/ping</code> 的请求。验证成功后,它将把请求转发给我们的 go HTTP server。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/secure/ping -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ0ZXN0LWtleSIsImV4cCI6MTY0MjU3ODU5M30.IYudBr7FTgRme70u4rEBoYNtGmGByzgfGlt8hctI__Q' -i HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 12 Connection: keep-alive Date: Tue, 18 Jan 2022 08:00:04 GMT Server: APISIX/2.11.0 secure/pong </code></pre> <p>任何无效的 JWT 请求都会抛出 <code>HTTP 401 Unauthorized</code> 的错误。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/secure/ping -i HTTP/1.1 401 Unauthorized Date: Tue, 18 Jan 2022 08:00:33 GMT Content-Type: text/plain; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Server: APISIX/2.11.0 {"message":"Missing JWT token in request"} </code></pre> <h3>Vault 与 APISIX jwt-auth 插件集成的不同用例</h3> <p>Apache APISIX <code>jwt-auth</code> 插件可以被配置为从 Vault 存储中获取简单的文本密钥以及 RS256 公私密钥对。</p> <p>:::note 对于该集成支持的早期版本,该插件希望存储到金库路径中的密钥名称在 [<code>secret</code>, <code>public_key</code>, <code>private_key</code>] 之间,以成功使用该密钥。在未来的版本中,我们将增加对引用自定义命名的密钥的支持。 :::</p> <ol> <li> <p>你在 Vault 内存储了 HS256 签名密钥,你想用它来进行 jwt 签名和验证。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "jack", "plugins": { "jwt-auth": { "key": "key-1", "vault": {} } } }' </code></pre> <p>在这里,插件在 Consumer 配置中提到的 Consumer 用户 <code>jack</code> 的 Vault 路径(<code>&lt;vault.prefix from conf.yaml&gt;/consumer/jack/jwt-auth</code>)中查找密钥 <code>secret</code>,并使用它进行后续的签名和 jwt 验证。如果在同一路径中没有找到密钥,该插件将记录一个错误,并且无法执行 JWT 验证。</p> </li> <li> <p>RS256 RSA 密钥对,公钥和私钥都存储在 Vault 中。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "jim", "plugins": { "jwt-auth": { "key": "rsa-keypair", "algorithm": "RS256", "vault": {} } } }' </code></pre> <p>The plugin looks up for <code>public_key</code> and <code>private_key</code> keys inside vault kv path (<code>&lt;vault.prefix from conf.yaml&gt;/consumer/jim/jwt-auth</code>) for <code>jim</code> mentioned inside plugin vault configuration. If not found, authentication fails. 该插件在 Vault 键值对 路径(<code>&lt;vault.prefix from conf.yaml&gt;/consumer/jim/jwt-auth</code>)中为插件 Vault 配置中提到的用户 <code>jim</code> 查找 <code>public_key</code> 和 <code>private_key</code>。如果没有找到,认证失败。</p> <p>如果你不确定如何将公钥和私钥存储到 Vault 键值对中,请使用这个命令。</p> <pre><code class="language-shell"># provided, your current directory contains the files named "public.pem" and "private.pem" $ vault kv put kv/apisix/consumer/jim/jwt-auth public_key=@public.pem private_key=@private.pem Success! Data written to: kv/apisix/consumer/jim/jwt-auth </code></pre> </li> <li> <p>Consumer 配置中的公钥,而私钥在 Vault 中。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "john", "plugins": { "jwt-auth": { "key": "user-key", "algorithm": "RS256", "public_key": "-----BEGIN PUBLIC KEY-----\n……\n-----END PUBLIC KEY-----" "vault": {} } } }' </code></pre> <p>这个插件使用来自 Consumer 配置的 RSA 公钥,并使用直接从 Vault 获取的私钥。</p> </li> </ol> <h3>禁用 Vault 插件</h3> <p>现在,要禁用 <code>jwt-auth</code> 插件的 Vault 查询,只需从 Consumer 插件配置中删除空的 Vault 对象(本例中是 <code>jack</code>)。这将使 JWT 插件在随后对已启用 <code>jwt-auth</code> 配置的 URI 路由的请求中,将查找签名密钥(包括 HS256/HS512 或 RS512 密钥对)纳入插件配置。即使你在 APISIX <code>config.yaml</code> 中启用了 Vault 配置,也不会有请求被发送到 Vault server。</p> <p>Apache APISIX 插件是热加载的,因此不需要重新启动 Apache APISIX,配置可以立即生效。</p> <pre><code class="language-shell">$ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "username": "jack", "plugins": { "jwt-auth": { "key": "test-key", "secret": "my-secret-key" } } }' </code></pre> <h2>结语</h2> <p>本文为大家带来了 Apache APISIX 即将发布的 Vault 插件以及相关细节。</p> <p>欢迎随时在 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 中发起讨论,或通过<a href="https://apisix.apache.org/zh/docs/general/subscribe-guide">邮件列表</a>进行交流。</p>

多协议接入框架 xRPC 发布在即,为你解读更多 APISIX 生态细节

<p>随着业务场景和需求越来越复杂和多样化,越来越多的标准和协议都开始崭露头角。Apache APISIX 作为 Apache 基金会的顶级开源项目,一直积极参与并推进相关生态层面的扩展。</p> <p>本文将从<strong>多协议代理</strong>与<strong>多语言支持</strong>两个角度,为大家带来 Apache APISIX 即将发布的 xRPC 框架与多语言插件的相关示例。</p> <h2>多协议代理</h2> <p>在 Apache APISIX 中,每个请求都会对应一个 Route 对象。目前 Apache APISIX 的代理场景主要以下两种。</p> <p><img src="https://static.apiseven.com/202108/1642732975469-74071c65-e869-4133-857f-822b58d6b86e.png" alt="目前 APISIX 两种代理场景" referrerpolicy="no-referrer"></p> <p>第一种是 HTTP 协议代理,也是目前 APISIX 中最常用的请求代理。基于 HTTP 协议代理,Apache APISIX 目前已经实现了数十种流量治理能力,如:细粒度的流控、安全和可观测性等。</p> <p>第二种则是基于 TCP 和 UDP 的动态协议代理和负载均衡,它提供了最基础的流量准入能力和链接级别的日志能力。这种代理模式可以代理任何基于 TCP/UDP 协议的请求,如:MySQL、Redis、Mongo 或 DNS 等。但由于它是基于 TCP/UDP 的代理没有上层的应用层协议,只能获取到四元组的一些基础信息,所以在扩展能力上会稍弱一些。</p> <h3>为什么要开发 xRPC 框架</h3> <p>由于 Stream Route 在协议代理上的限制,加之我们希望在 APISIX 上可以支持更多的应用层协议,以服务更多用户和应用场景,xRPC 框架应运而生。</p> <p>通过 xRPC 框架可以非常便捷地扩展协议能力,不管是特定还是私有数据协议,都可以具备类似 HTTP 协议代理的<strong>精准颗粒度</strong>的和<strong>更高阶的 7 层控制</strong>,比如请求级别的可观测性、高级访问控制和代理策略等功能。</p> <h3>什么是 xRPC</h3> <p>xRPC 从字面角度来分析,即 X 为协议资源的抽象代表。而 RPC 是我们认为所有经过网关的资源都为一个过程调度,即它是一个协议扩展框架。所以在定位上,xRPC 是一个基础框架,而不是一种具体协议的实现。</p> <p><img src="https://static.apiseven.com/202108/1642733068660-f479ffcc-5bda-49de-bbd9-0d04d7259450.png" alt="xRPC 架构图" referrerpolicy="no-referrer"></p> <p>从上图架构可以看出,xRPC 是基于 APISIX Core 扩展出来的框架。在该框架的基础之上,用户可以去实现不同应用层的协议,比如 Redis、MySQL、Mongo 和 Postgres 等。而在不同的协议之上,又可以去抽象一些共性因素,实现相关插件能力,让不同的协议可以共享这些能力。</p> <p>所以 xRPC 的主要作用可以总结为:<strong>提供标准化应用层协议的接入能力,支持跨协议的能力共享,以及让用户可以获得自定义协议的扩展能力</strong>。</p> <h4>应用场景示例</h4> <p>有了 xRPC 协议框架之后,它可以解决哪些场景呢?这里简单给大家举几个例子。</p> <ol> <li>示例 1 :像 Redis 在早期版本中是不支持 TLS 的。如果我们系统里同时存在多个版本的 Redis,同时因为某些原因暂时不能在生产环境中升级 Redis,但又有增加 TLS 能力的需求。这个时候就可以基于 xPRC 的 Redis Protocol 来解决上述情况。</li> <li>示例 2:当我们想对某些 IP 或者调用方做频率限制并且想直观的看到每个调用来源的调用频率,这些 Redis 自身是不支持的。此时就可以通过那通过 xRPC 扩展出来的 Redis Protocol 完美解决。</li> <li>示例 3:如果想利用 MySQL 临时开启慢查询功能,只需接入 MySQL Protocol 并在 APISIX 配置相关策略即可,省去了后续再一台台机器去登录实例的繁琐步骤。</li> </ol> <p>当然,类似的应用场景还很多,也希望在功能发布之后,大家多多在实际的应用过程中去体验和实践。接入 xPRC 后的调用过程如下图所示。</p> <p><img src="https://static.apiseven.com/202108/1643103835579-d215a120-f62e-4ba5-aa14-59ea3d38a429.png" alt="调用过程" referrerpolicy="no-referrer"></p> <p>一旦通过 Apache APISIX 完成了上游服务的接管,就可以把上游不同的应用服务进行统一化管理。类似日志输出、监控、安全还有问题排查等功能,都可以通过一套标准化的策略来完成。</p> <h4>计划实现阶段</h4> <p>目前 Apache APISIX xRPC 框架的设计,初步划分为 5 个阶段。</p> <p><img src="https://static.apiseven.com/202108/1643103835583-40afb0a0-ec20-40e8-84de-b34afee2724c.png" alt="计划实现阶段图" referrerpolicy="no-referrer"></p> <ul> <li>阶段一:Read 读取数据与协议解码。</li> <li>阶段二:Access Phase 准入阶段。提供插件接入功能,可实现安全、流控和准入等需求场景。</li> <li>阶段三:Proxy 数据转发与负载均衡。提供自定义负载均衡策略及算法的接入支持。</li> <li>阶段四:Send 发送数据与协议编码。</li> <li>阶段五:Log Phase 日志阶段。提供插件接入功能,实现日志上报和记录等需求场景。</li> </ul> <h2>多语言生态</h2> <p>为了满足日益丰富和庞大的计算语言库,打造多语言环境的代码支持成为应对未来技术发展的第一门槛。Apache APISIX 在多语言开发的道路上也做了很多的探索与实践。</p> <p>比如在近期已经实现了对 <strong>WebAssembly</strong> 的支持,具体实现细节与功能可参考「<a href="https://apisix.apache.org/zh/blog/2021/11/19/apisix-supports-wasm">Apache APISIX 拥抱 WASM 生态」</a>文章。当然,目前 Apache APISIX 对 WASM 的功能支持还属于实验阶段,未来仍会继续完善对 WASM 的相关支持。如果您对此项目感兴趣,也欢迎积极参与到 <a href="https://github.com/api7/wasm-nginx-module">wasm-nginx-module</a> 项目贡献中。</p> <p>同时,在对 WASM 实现支持前,Apache APISIX 已通过 「xPluginRunner 多语言插件运行时」实现了对多种开发语言的支持。即在开发 APISIX 插件时,用户不仅可以使用 APISIX 原生支持的 Lua 代码去编写,也可以使用各自熟悉的语言,比如 Go、Java 和 Python 等,实现对 APISIX 插件的编写与扩展。</p> <h3>xPluginRunner</h3> <p><img src="https://static.apiseven.com/202108/1642733411405-19b13181-0f5e-46af-837b-66e485f2e0b0.png" alt="xPluginRunner 实现方式图" referrerpolicy="no-referrer"></p> <p>xPluginRunner 的实现方式如上图所示。整个通信过程是在内置插件「开始执行之前」和「完成执行之后」,APISIX 会发起本地 RPC 请求到各语言的插件运行时。在插件运行时中,实现各个插件内的计算和策略处理,最后将结果响应给 APISIX,基于响应结果再进行后续的决策。</p> <p>xPluginRunner 作为跟 Apache APISIX 通信的桥梁,主要实现了<strong>私有数据协议的解析与 RPC 报文的封包与解包</strong>。</p> <p>目前 Apache APISIX xPluginRunner 的方案已经处于比较稳定的阶段了,从社区反馈中也得知部分企业已经开始尝试在生产环境中应用了。如果您对此项目感兴趣,也欢迎积极参与到各个开发语言插件项目中:</p> <ul> <li><a href="https://github.com/apache/apisix-go-plugin-runner">apisix-go-plugin-runner</a></li> <li><a href="https://github.com/apache/apisix-java-plugin-runner">apisix-java-plugin-runner</a></li> <li><a href="https://github.com/apache/apisix-python-plugin-runner">apache-apisix-python-runner</a></li> </ul> <p>最后我们将通过一个简单的 Java 示例,为大家展示一下如何基于 Java Plugin Runner 来开发 APISIX 插件。</p> <h3>Java Plugin Runner 示例</h3> <p>首先在开发插件时,我们需要去实现 PluginFilter 的 Interface。Interface 中 <code>name</code> 方法主要用来标识和提取插件名称,<code>filter</code> 方法则用来过滤请求(也就是执行插件主体逻辑)。</p> <p><img src="https://static.apiseven.com/202108/1642733591297-642091b2-d4c7-4098-b7ff-41ffa5a2e00a.png" alt="Plugin" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/202108/1642733657248-5b7db563-f95f-4683-997e-47e76eeda4d9.png" alt="Interface" referrerpolicy="no-referrer"></p> <p>需要额外注意一点,上述代码中出现的 <code>request</code> 和 <code>response</code> 两个参数在 Runner 中存在固定逻辑(所有 Runner 都适用):</p> <ol> <li>如果希望请求继续转发,仅进行一些策略设置(如改写请求参数、头信息等),只需操作 <code>request</code> 对象即可。</li> <li>如果想要终止请求,可以通过 <code>response</code> 对象来完成(如设置响应体、响应头、状态码等)。</li> </ol> <p>APISIX 一旦感知到 Runner 中的 <code>response</code> 对象被操作就会立即终止当前请求。</p> <p>插件开发完成之后,就可以在 APISIX 中进行应用的实践了。首先将 Runner 及项目中的插件编译为 jar 包,并将 jar 包的绝对路径配置到 APISIX 主配置文件中,配置方式如下:</p> <p><img src="https://static.apiseven.com/202108/1642733807923-9e3ad231-0094-4e37-a830-29973b43e495.png" alt="将 Runner jar 配置到 APISIX 主配置" referrerpolicy="no-referrer"></p> <p>最后重启 Apache APISIX,就可以进行路由和插件的配置及请求验证环节了。</p> <p><img src="https://static.apiseven.com/202108/1642733908224-64f3ec2c-6d33-4130-b8b6-0fe10e00c48e.png" alt="路由和插件配置" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/202108/1642733944940-69b06c71-22f7-45e4-9b6d-7f1b62167180.png" alt="请求验证" referrerpolicy="no-referrer"></p> <h2>总结</h2> <p>本文为大家带来了 Apache APISIX 即将发布的 xRPC 框架以及相关细节,同时介绍了 Apache APISIX 在多语言开发支持中的细节展示。通过多协议代理与多语言支持两个角度,充分展示了 Apache APISIX 在面向生态的多项努力。也欢迎随时在 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 中发起讨论,或通过<a href="https://apisix.apache.org/zh/docs/general/subscribe-guide">邮件列表</a>进行交流。</p>

从原理到操作,让你在 Apache APISIX 中代理 Dubbo 服务更便捷

<h2>背景</h2> <p><a href="https://dubbo.apache.org/zh/">Apache Dubbo</a> 是由阿里巴巴开源并捐赠给 Apache 的微服务开发框架,它提供了 RPC 通信与微服务治理两大关键能力。不仅经过了阿里电商场景中海量流量的验证,也在国内的技术公司中被广泛落地。</p> <p>在实际应用场景中,Apache Dubbo 一般会作为后端系统间 RPC 调用的实现框架,当需要提供 HTTP 接口给到前端时,会通过一个「胶水层」将 Dubbo Service 包装成 HTTP 接口,再交付到前端系统。</p> <p><a href="https://apisix.apache.org/">Apache APISIX</a> 是 Apache 软件基金会的顶级开源项目,也是当前最活跃的开源网关项目。作为一个动态、实时、高性能的开源 API 网关,Apache APISIX 提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。</p> <p>得益于 Apache Dubbo 的应用场景优势,Apache APISIX 基于开源项目 tengine/mod_dubbo 模块为 Apache Dubbo 服务配备了HTTP 网关能力。通过 dubbo-proxy 插件,可以轻松地将 Dubbo Service 发布为 HTTP 服务。</p> <p><img src="https://static.apiseven.com/202108/1641873664933-65d02cb3-ec3e-4b95-945d-91cfb8106751.png" alt="架构图" referrerpolicy="no-referrer"></p> <h2>如何使用</h2> <h3>入门篇:安装使用</h3> <p>这里我们建议使用 Apache APISIX 2.11 版本镜像进行安装。该版本的 APISIX-Base 中已默认编译了 Dubbo 模块,可直接使用 <code>dubbo-proxy</code> 插件。</p> <p>在接下来的操作中,我们将使用 <a href="https://github.com/apache/dubbo-samples"><code>dubbo-samples</code></a> 项目进行部分展示。该项目是一些使用 Apache Dubbo 实现的 Demo 应用,本文中我们采用其中的 <code>dubbo-samples-tengine</code> 子模块作为 Dubbo Provider。</p> <p>在进入正式操作前,我们先简单看下 Dubbo 接口的定义、配置以及相关实现。</p> <h4>接口实现一览</h4> <pre><code class="language-java">public interface DemoService { /** * standard samples tengine dubbo infterace demo * @param context tengine pass http infos * @return Map&lt;String, Object&gt;&lt;/&gt; pass to tengine response http **/ Map&lt;String, Object&gt; tengineDubbo(Map&lt;String, Object&gt; httpRequestContext); } </code></pre> <p>如上所示,Dubbo 接口的定义是固定的。即方法参数中 <code>Map</code> 表示 APISIX 传递给 Dubbo Provider 关于 HTTP request 的一些信息(如:header、body...)。而方法返回值的 <code>Map</code> 表示 Dubbo Provider 传递给 APISIX 要如何返回 HTTP response 的一些信息。</p> <p>接口信息之后可通过 XML 配置方式发布 DemoService。</p> <pre><code class="language-xml">&lt;!-- service implementation, as same as regular local bean --&gt; &lt;bean id="demoService" class="org.apache.dubbo.samples.tengine.provider.DemoServiceImpl"/&gt; &lt;!-- declare the service interface to be exported --&gt; &lt;dubbo:service interface="org.apache.dubbo.samples.tengine.DemoService" ref="demoService"/&gt; </code></pre> <p>通过上述配置后,Consumer 可通过 <code>org.apache.dubbo.samples.tengine.DemoService</code> 访问其中的<code>tengineDubbo</code> 方法。具体接口实现如下:</p> <pre><code class="language-java">public class DemoServiceImpl implements DemoService { @Override public Map&lt;String, Object&gt; tengineDubbo(Map&lt;String, Object&gt; httpRequestContext) { for (Map.Entry&lt;String, Object&gt; entry : httpRequestContext.entrySet()) { System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); } Map&lt;String, Object&gt; ret = new HashMap&lt;String, Object&gt;(); ret.put("body", "dubbo success\n"); // http response body ret.put("status", "200"); // http response status ret.put("test", "123"); // http response header return ret; } } </code></pre> <p>上述代码中,<code>DemoServiceImpl</code> 会打印接收到的 <code>httpRequestContext</code>,并通过返回包含有指定 Key 的 Map 对象去描述该 Dubbo 请求的 HTTP 响应。</p> <h4>操作步骤</h4> <ol> <li>启动 <a href="https://github.com/apache/dubbo-samples/tree/master/dubbo-samples-tengine#install-dubbo"><code>dubbo-samples-tengine</code></a>。</li> <li>在 <code>config.yaml</code> 文件中进行 <code>dubbo-proxy</code> 插件启用。</li> </ol> <pre><code class="language-yaml"># Add this in config.yaml plugins: - ... # plugin you need - dubbo-proxy </code></pre> <ol start="3"> <li>创建指向 Dubbo Provider 的 Upstream。</li> </ol> <pre><code class="language-shell">curl http://127.0.0.1:9180/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "nodes": { "127.0.0.1:20880": 1 }, "type": "roundrobin" }' </code></pre> <ol start="4"> <li>为 DemoService 暴露一个 HTTP 路由。</li> </ol> <pre><code class="language-shell">curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "host": "example.org" "uris": [ "/demo" ], "plugins": { "dubbo-proxy": { "service_name": "org.apache.dubbo.samples.tengine.DemoService", "service_version": "0.0.0", "method": "tengineDubbo" } }, "upstream_id": 1 }' </code></pre> <ol start="5"> <li>使用 curl 命令请求 Apache APISIX,并查看返回结果。</li> </ol> <pre><code class="language-shell">curl http://127.0.0.1:9080/demo -H "Host: example.org" -X POST --data '{"name": "hello"}' &lt; HTTP/1.1 200 OK &lt; Date: Sun, 26 Dec 2021 11:33:27 GMT &lt; Content-Type: text/plain; charset=utf-8 &lt; Content-Length: 14 &lt; Connection: keep-alive &lt; test: 123 &lt; Server: APISIX/2.11.0 &lt; dubbo success </code></pre> <p>说明:上述代码返回中包含了 <code>test: 123</code> Header,以及 <code>dubbo success</code> 字符串作为 Body 体。这与我们在 <code>DemoServiceImpl</code> 编码的预期效果一致。</p> <ol start="6"> <li>查看 Dubbo Provider 的日志。</li> </ol> <pre><code>Key = content-length, Value = 17 Key = host, Value = example.org Key = content-type, Value = application/x-www-form-urlencoded Key = body, Value = [B@70754265 Key = accept, Value = */* Key = user-agent, Value = curl/7.80.0 </code></pre> <p>说明:通过 <code>httpRequestContext</code> 可以拿到 HTTP 请求的 Header 和 Body。其中 Header 会作为 Map 元素,而 Body 中 Key 值是固定的字符串"body",Value 则代表 Byte 数组。</p> <h3>进阶篇:复杂场景示例</h3> <p>在上述的简单用例中可以看出,我们确实通过 Apache APISIX 将 Dubbo Service 发布为一个 HTTP 服务,但是在使用过程中的限制也非常明显。比如:接口的参数和返回值都必须要是 <code>Map&lt;String, Object&gt;</code>。</p> <p>那么,如果项目中出现已经定义好、但又不符合上述限制的接口,该如何通过 Apache APISIX 来暴露 HTTP 服务呢?</p> <h4>操作步骤</h4> <p>针对上述场景,我们可以通过 HTTP Request Body 描述要调用的 Service 和 Method 以及对应参数,再利用 Java 的反射机制实现目标方法的调用。最后将返回值序列化为 JSON,并写入到 HTTP Response Body 中。</p> <p>这样就可以将 Apache APISIX 的 「HTTP to Dubbo」 能力进一步加强,并应用到所有已存在的 Dubbo Service 中。具体操作可参考下方:</p> <ol> <li>为已有项目增加一个 Dubbo Service 用来统一处理 HTTP to Dubbo 的转化。</li> </ol> <pre><code class="language-java">public class DubboInvocationParameter { private String type; private String value; } public class DubboInvocation { private String service; private String method; private DubboInvocationParameter[] parameters; } public interface HTTP2DubboService { Map&lt;String, Object&gt; invoke(Map&lt;String, Object&gt; context) throws Exception; } @Component public class HTTP2DubboServiceImpl implements HTTP2DubboService { @Autowired private ApplicationContext appContext; @Override public Map&lt;String, Object&gt; invoke(Map&lt;String, Object&gt; context) throws Exception { DubboInvocation invocation = JSONObject.parseObject((byte[]) context.get("body"), DubboInvocation.class); Object[] args = new Object[invocation.getParameters().size()]; for (int i = 0; i &lt; args.length; i++) { DubboInvocationParameter parameter = invocation.getParameters().get(i); args[i] = JSONObject.parseObject(parameter.getValue(), Class.forName(parameter.getType())); } Object svc = appContext.getBean(Class.forName(invocation.getService())); Object result = svc.getClass().getMethod(invocation.getMethod()).invoke(args); Map&lt;String, Object&gt; httpResponse = new HashMap&lt;&gt;(); httpResponse.put("status", 200); httpResponse.put("body", JSONObject.toJSONString(result)); return httpResponse; } } </code></pre> <ol start="2"> <li>通过如下命令请求来发起相关调用。</li> </ol> <pre><code class="language-shell">curl http://127.0.0.1:9080/demo -H "Host: example.org" -X POST --data ' { "service": "org.apache.dubbo.samples.tengine.DemoService", "method": "createUser", "parameters": [ { "type": "org.apache.dubbo.samples.tengine.User", "value": "{'name': 'hello'}" } ] }' </code></pre> <h2>总结</h2> <p>本文为大家介绍了如何借助 Apache APISIX 实现 Dubbo Service 的代理,通过引入 <code>dubbo-proxy</code> 插件便可为 Dubbo 框架的后端系统构建更简单更高效的流量链路。</p> <p>希望通过上述操作步骤和用例场景分享,能为大家在相关场景的使用提供借鉴思路。更多关于 <code>dubbo-proxy</code> 插件的介绍与使用可参考<a href="https://apisix.apache.org/docs/apisix/plugins/dubbo-proxy/">官方文档</a>。</p>

Apache APISIX 结合 Authing 实现集中式身份认证管理

<h2>项目介绍</h2> <h3>关于 Apache APISIX</h3> <p><a href="https://github.com/apache/apisix">Apache APISIX</a> 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。Apache APISIX 不仅支持插件动态变更和热插拔,而且拥有众多实用的插件。Apache APISIX 的 OpenID Connect 插件支持 OpenID Connect 协议,用户可以使用该插件让 Apache APISIX 对接 Authing 服务,作为集中式认证网关部署于企业中。</p> <h3>关于 Authing</h3> <p><a href="https://www.authing.cn/">Authing</a> 是国内首款以开发者为中心的全场景身份云产品,集成了所有主流身份认证协议,为企业和开发者提供完善安全的用户认证和访问管理服务。以「API First」作为产品基石,把身份领域所有常用功能都进行了模块化的封装,通过全场景编程语言 SDK 将所有能力 API 化提供给开发者。同时,用户可以灵活的使用 Authing 开放的 RESTful APIs 进行功能拓展,满足不同企业不同业务场景下的身份管理需求。</p> <h2>什么是集中式身份认证</h2> <h3>传统身份认证</h3> <p>在传统认证模式下,各个后端应用服务需要单独开发功能以支持身份认证功能,例如与身份提供商进行交互、获取用户的身份信息等功能。</p> <p><img src="https://static.apiseven.com/202108/1639467045776-715e1805-540b-4cef-87c5-6166e2af43a8.png" alt="传统认证模式流程图" referrerpolicy="no-referrer"></p> <h3>集中式身份认证</h3> <p>与传统认证模式不同,集中身份认证模式把用户认证从应用服务中抽离了出来。以 Apache APISIX 为例,集中认证的流程如上图所示:首先由用户发起请求(request),然后由前置的网关负责用户认证流程,与身份提供方对接,向身份提供方发送身份认证(authorization)请求。身份提供方返回用户身份信息(user info)。网关完成用户身份识别后,将用户身份信息通过请求头的形式转发至后端应用。</p> <p><img src="https://static.apiseven.com/202108/1639467122244-d4292436-c5ce-48f6-b1d5-67645f24fbc9.png" alt="集中认证模式流程图" referrerpolicy="no-referrer"></p> <h3>集中式身份认证的优点</h3> <p>相比较传统认证模式,集中认证模式下有如下优点:</p> <ol> <li>简化应用开发流程,降低开发应用工作量和维护成本,避免各个应用重复开发身份认证逻辑。</li> <li>提高业务的安全性,集中身份认证模式在网关层面能够及时拦截未经身份认证的请求,保护后端的应用。</li> </ol> <p>同时结合 Authing 强大的身份认证管理功能,可实现如下功能:</p> <ol> <li>通过控制台对身份认证服务进行生命周期管理,包括创建、启用、禁用等。</li> <li>提供实时、可视化的应用监控,包括:接口请求次数、接口调用延迟和接口错误信息,并且进行实时告警通知。</li> <li>集中式日志,可以方便地查看用户登录、登出以及对应用的调整和修改信息。</li> </ol> <p>更多具体内容可参考 <a href="https://www.authing.cn/gateway-integration">Authing 应用集成网关</a>。</p> <h2>如何使用</h2> <h3>步骤一:配置 Authing</h3> <ol> <li> <p>登录 Authing 账号,选择自建应用,并填入应用名称和认证地址。如果你没有 Authing 账号,请访问 <a href="https://www.authing.cn/">Authing 官网</a>,单击右上角 “登录/注册”,注册一个 Authing 账号。 <img src="https://static.apiseven.com/202108/1641275135400-5a11226a-404d-43f7-bde5-6e1b9599cebd.png" alt="配置 Authing" referrerpolicy="no-referrer"></p> </li> <li> <p>单击“创建”,创建一个 Authing 应用。 <img src="https://static.apiseven.com/202108/1641275174810-f78469f7-24dd-41c8-bdc7-eb7d5ff58672.png" alt="创建 Authing 应用" referrerpolicy="no-referrer"></p> </li> <li> <p>设置登录和登出的跳转 URL。在认证过程中,Authing 将会拒绝除配置以外的回调 URL,由于本次为本地测试,所以将登录回调 URL 和登出回调 URL 都设置为 APISIX 访问地址 http://127.0.0.1:9080/ 。 <img src="https://static.apiseven.com/202108/1641275642514-65a2205a-22dd-43b2-ba06-e51b249754a8.png" alt="设置登录和登出的跳转 URL" referrerpolicy="no-referrer"></p> </li> <li> <p>创建用户(可选)。在用户列表页面,创建用户,账号密码分别为 user1/user1,并且可以在「用户信息-授权管理」页面中设置是否允许应用的访问(默认为允许)。 <img src="https://static.apiseven.com/202108/1641275703261-ec7d98d4-50ee-428c-b73c-a70002e67dfb.png" alt="创建用户" referrerpolicy="no-referrer"> <img src="https://static.apiseven.com/202108/1641275703263-90176798-6bb7-40d5-976f-dfc516db020f.png" alt="设置访问权限" referrerpolicy="no-referrer"></p> </li> <li> <p>访问应用页面,获取以下配置,配置 Apache APISIX OpenID Connect 时需要提供这些信息:</p> <ol> <li>App ID: OAuth client ID,即应用的 ID。与下文的 <code>client_id</code> 和 <code>{YOUR_CLIENT_ID}</code> 对应。</li> <li>App secret:OAuth client secret,即应用密钥。与下文的 <code>client_secret</code> 和 <code>{YOUR_CLIENT_SECRET}</code> 对应。</li> <li>服务发现地址:应用服务发现的地址。与下文的 <code>{YOUR_DISCOVERY}</code> 对应。 <img src="https://static.apiseven.com/202108/1641275800242-e937cfd4-6237-4928-adab-7ec99556bbac.png" alt="配置信息" referrerpolicy="no-referrer"></li> </ol> </li> </ol> <h3>步骤二:安装 Apache APISIX</h3> <p>你可以通过源码包、Docker、Helm Chart 等多种方式来安装 Apache APISIX。</p> <h4>安装依赖</h4> <p>Apache APISIX 的运行环境需要依赖 NGINX 和 etcd,所以在安装 Apache APISIX 前,请根据您使用的操作系统安装对应的依赖。我们提供了 CentOS7、Fedora 31 &amp; 32 、Ubuntu 16.04 &amp; 18.04、 Debian 9 &amp; 10 和 MacOS 上的依赖安装操作步骤,详情请参考<a href="https://apisix.apache.org/zh/docs/apisix/install-dependencies/">安装依赖</a>。</p> <p>通过 Docker 或 Helm Chart 安装 Apache APISIX 时,已经包含了所需的 NGINX 和 etcd,请参照各自对应的文档。</p> <h4>通过 RPM 包安装(CentOS 7)</h4> <p>这种安装方式适用于 CentOS 7 操作系统,请运行以下命令安装 Apache APISIX。</p> <pre><code class="language-shell">sudo yum install -y https://github.com/apache/apisix/releases/download/2.7/apisix-2.7-0.x86_64.rpm </code></pre> <h4>通过 Docker 安装</h4> <p>详情请参考:<a href="https://hub.docker.com/r/apache/apisix">使用 Docker 安装 Apache APISIX</a>。</p> <h4>通过 Helm Chart 安装</h4> <p>详情请参考:<a href="https://github.com/apache/apisix-helm-chart">使用 Helm Chart 安装 Apache APISIX</a>。</p> <h4>通过源码包安装</h4> <ol> <li>创建一个名为 <code>apisix-2.7</code> 的目录:</li> </ol> <pre><code class="language-shell">mkdir apisix-2.7 </code></pre> <ol start="2"> <li>下载 Apache APISIX Release 源码包:</li> </ol> <pre><code class="language-shell">wget https://downloads.apache.org/apisix/2.7/apache-apisix-2.7-src.tgz </code></pre> <p>您也可以通过 Apache APISIX 官网下载 Apache APISIX Release 源码包。 Apache APISIX 官网也提供了 Apache APISIX、APISIX Dashboard 和 APISIX Ingress Controller 的源码包,详情请参考 <a href="https://apisix.apache.org/zh/downloads">Apache APISIX 官网-下载页</a>。</p> <ol start="3"> <li>解压 Apache APISIX Release 源码包:</li> </ol> <pre><code class="language-shell">tar zxvf apache-apisix-2.7-src.tgz -C apisix-2.7 </code></pre> <ol start="4"> <li>安装运行时依赖的 Lua 库:</li> </ol> <pre><code class="language-shell"># 切换到 apisix-2.7 目录 cd apisix-2.7 # 创建依赖 make deps </code></pre> <h4>初始化依赖</h4> <p>运行以下命令初始化 NGINX 配置文件和 etcd。</p> <pre><code class="language-shell"># initialize NGINX config file and etcd make init </code></pre> <h3>步骤三:启动 Apache APISIX 并配置对应的路由</h3> <ol> <li> <p>运行以下命令,启动 Apache APISIX。</p> <pre><code class="language-shell">apisix start </code></pre> </li> <li> <p>创建路由并配置 OpenID Connect 插件。OpenID Connect 配置列表如下。</p> </li> </ol> <table> <thead> <tr> <th style="text-align:left">字段</th> <th style="text-align:left">默认值</th> <th style="text-align:left">使用说明</th> </tr> </thead> <tbody> <tr> <td style="text-align:left">client_id</td> <td style="text-align:left">N/A</td> <td style="text-align:left">OAuth 客户端 ID</td> </tr> <tr> <td style="text-align:left">client_secret</td> <td style="text-align:left">N/A</td> <td style="text-align:left">OAuth 客户端密钥</td> </tr> <tr> <td style="text-align:left">discovery</td> <td style="text-align:left">N/A</td> <td style="text-align:left">身份提供商的服务发现端点</td> </tr> <tr> <td style="text-align:left">scope</td> <td style="text-align:left">openid</td> <td style="text-align:left">需要访问资源范围</td> </tr> <tr> <td style="text-align:left">relm</td> <td style="text-align:left">apisix</td> <td style="text-align:left">指定 WWW-Authenticate 响应头验证信息</td> </tr> <tr> <td style="text-align:left">bearer_only</td> <td style="text-align:left">false</td> <td style="text-align:left">是否检查请求头中的 token</td> </tr> <tr> <td style="text-align:left">logout_path</td> <td style="text-align:left">/logout</td> <td style="text-align:left">登出的 URI</td> </tr> <tr> <td style="text-align:left">redirect_uri</td> <td style="text-align:left">request_uri</td> <td style="text-align:left">身份提供商跳转回来的 URI,默认为请求地址</td> </tr> <tr> <td style="text-align:left">timeout</td> <td style="text-align:left">3</td> <td style="text-align:left">请求超时时间,单位为秒</td> </tr> <tr> <td style="text-align:left">ssl_verify</td> <td style="text-align:left">false</td> <td style="text-align:left">是否身份提供商的校验 ssl 证书</td> </tr> <tr> <td style="text-align:left">introspection_endpoint</td> <td style="text-align:left">N/A</td> <td style="text-align:left">身份提供商的令牌验证端点的 URL,不填则将从 discovery 响应中提取。</td> </tr> <tr> <td style="text-align:left">introspection_endpoint_auth_method</td> <td style="text-align:left">client_secret_basic</td> <td style="text-align:left">令牌自省的认证方法名称</td> </tr> <tr> <td style="text-align:left">public_key</td> <td style="text-align:left">N/A</td> <td style="text-align:left">验证令牌的公钥</td> </tr> <tr> <td style="text-align:left">token_signing_alg_values_expected</td> <td style="text-align:left">N/A</td> <td style="text-align:left">验证令牌的算法</td> </tr> <tr> <td style="text-align:left">set_access_token_header</td> <td style="text-align:left">true</td> <td style="text-align:left">是否在请求头中携带 access token</td> </tr> <tr> <td style="text-align:left">access_token_in_authorization_header</td> <td style="text-align:left">false</td> <td style="text-align:left">true 时将 access token 放置在 Authorization 头中,false 时将 access token 放置在 X-Access-Token 头中。</td> </tr> <tr> <td style="text-align:left">set_id_token_header</td> <td style="text-align:left">false</td> <td style="text-align:left">是否将 ID token 携带至 X-ID-Token 请求头</td> </tr> <tr> <td style="text-align:left">set_userinfo_header</td> <td style="text-align:left">false</td> <td style="text-align:left">是否将用户信息携带至 X-Userinfo 请求头</td> </tr> </tbody> </table> <p>以下代码示例通过 Apache APISIX Admin API 进行创建路由,设置路由的上游为 <a href="http://httpbin.org/">httpbin.org</a>。<code>httpbin.org</code> 是一个简单的用于接收请求和响应请求的后端服务,下文将使用 <code>httpbin.org</code> 的 <code>get</code> 页面,参考 <a href="http://httpbin.org/#/HTTP_Methods/get_get">http bin get</a>。</p> <p>具体配置项请参考 <a href="https://apisix.apache.org/zh/docs/apisix/plugins/openid-connect/">Apache APISIX OpenID Connect Plugin</a>。</p> <pre><code class="language-shell">curl -XPOST 127.0.0.1:9080/apisix/admin/routes -H "X-Api-Key: edd1c9f034335f136f87ad84b625c8f1" -d '{ "uri":"/*", "plugins":{ "openid-connect":{ "client_id":"{YOUR_CLIENT_ID}", "client_secret":"{YOUR_CLIENT_SECRET}", "discovery":"https://{YOUR_DISCOVERY}", "scope":"openid profile", "bearer_only":false, "realm":"apisix", "introspection_endpoint_auth_method":"client_secret_post", "redirect_uri":"http://127.0.0.1:9080/" } }, "upstream":{ "type":"roundrobin", "nodes":{ "httpbin.org:80":1 } } }' </code></pre> <h3>步骤四:访问 Apache APISIX</h3> <ol> <li> <p>访问 "http://127.0.0.1:9080/get",由于已经开启了 OpenID Connect 插件,所以页面被重定向到 Authing 登录页面(可在 Authing 控制台中 「应用-品牌化」对该页面进行定制)。 <img src="https://static.apiseven.com/202108/1641276796088-48cb415e-d5dd-4c15-a3da-ecbb4c987b06.png" alt="访问 Apache APISIX" referrerpolicy="no-referrer"></p> </li> <li> <p>输入用户在 Authing 注册的账号密码,或者在步骤一中创建的用户 user1/user1 ,单击“登录”,登录 Authing 账户。</p> </li> <li> <p>登录成功之后,能成功访问到 httpbin.org 中的 get 页面。该 httpbin.org/get 页面将返回请求的数据如下:</p> <pre><code class="language-shell">... "X-Access-Token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InFqeU55aVdVd2NhbUFxdEdVRUNCeFNsTWxQSWtTR2N1NmkyZzhEUk1OSGsifQ.eyJqdGkiOiJjTy16a0pCS0NSRFlHR2kyWkJhY0oiLCJzdWIiOiI2MWM5OGFmOTg0MjI4YWU0OTYyMDU4NTIiLCJpYXQiOjE2NDA1OTg4NTgsImV4cCI6MTY0MTgwODQ1OCwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSIsImlzcyI6Imh0dHBzOi8vYXBpc2l4LmF1dGhpbmcuY24vb2lkYyIsImF1ZCI6IjYxYzk4M2M0YjI4NzdkNDg2OWRkOGFjYiJ9.l2V8vDWcCObB1LjIhKs2ARG4J7WuB-0c-bnYZG2GP2zcpl6PMAPcId2B76CaXCU58ajGcfRmOlWJ67UaHrfWKv8IM4vcYN1gwhKdokSyrhEM31gQE-MzNEsEbPaVIGXdpR1N2JnAJK5-tKIjopDAXSwArfO6fQKTpjLhCi3COIA169WGRR4CKCwNzzpFAYP2ilNc18D_HRTBLS6UjxZSNUtWE5dbx7uBjblhwIwn5e1fxiEQcknVK8Dxf8NUliFECvr02HX2hNvmuCECkvA_mZYlshAeqidK8tSEXirAWsWS5jlXFqLiBJkhSHFrbxRyqeOSfJCJR_YcCwk9AzgZGg", "X-Id-Token": "eyJhdF9oYXNoIjoiRl8tRjZaUVgtWVRDNEh0TldmcHJmUSIsImJpcnRoZGF0ZSI6bnVsbCwiZmFtaWx5X25hbWUiOm51bGwsImdlbmRlciI6IlUiLCJnaXZlbl9uYW1lIjpudWxsLCJpc3MiOiJodHRwczpcL1wvYXBpc2l4LmF1dGhpbmcuY25cL29pZGMiLCJwaWN0dXJlIjoiaHR0cHM6XC9cL2ZpbGVzLmF1dGhpbmcuY29cL2F1dGhpbmctY29uc29sZVwvZGVmYXVsdC11c2VyLWF2YXRhci5wbmciLCJwcmVmZXJyZWRfdXNlcm5hbWUiOm51bGwsInVwZGF0ZWRfYXQiOiIyMDIxLTEyLTI3VDA5OjU0OjE3Ljc3M1oiLCJ3ZWJzaXRlIjpudWxsLCJ6b25laW5mbyI6bnVsbCwibmFtZSI6bnVsbCwiaWF0IjoxNjQwNTk4ODU4LCJuaWNrbmFtZSI6bnVsbCwibm9uY2UiOiJmMTlmZjhjODM5NzdmZjNlMDczMzZmMzg3Y2QxM2EzMSIsIm1pZGRsZV9uYW1lIjpudWxsLCJleHAiOjE2NDE4MDg0NTgsInN1YiI6IjYxYzk4YWY5ODQyMjhhZTQ5NjIwNTg1MiIsImxvY2FsZSI6bnVsbCwiYXVkIjoiNjFjOTgzYzRiMjg3N2Q0ODY5ZGQ4YWNiIiwicHJvZmlsZSI6bnVsbH0=", "X-Userinfo": "eyJ3ZWJzaXRlIjpudWxsLCJ6b25laW5mbyI6bnVsbCwibmFtZSI6bnVsbCwicHJvZmlsZSI6bnVsbCwibmlja25hbWUiOm51bGwsInN1YiI6IjYxYzk4YWY5ODQyMjhhZTQ5NjIwNTg1MiIsImxvY2FsZSI6bnVsbCwiYmlydGhkYXRlIjpudWxsLCJmYW1pbHlfbmFtZSI6bnVsbCwiZ2VuZGVyIjoiVSIsImdpdmVuX25hbWUiOm51bGwsIm1pZGRsZV9uYW1lIjpudWxsLCJwaWN0dXJlIjoiaHR0cHM6XC9cL2ZpbGVzLmF1dGhpbmcuY29cL2F1dGhpbmctY29uc29sZVwvZGVmYXVsdC11c2VyLWF2YXRhci5wbmciLCJwcmVmZXJyZWRfdXNlcm5hbWUiOm51bGwsInVwZGF0ZWRfYXQiOiIyMDIxLTEyLTI3VDA5OjU0OjE3Ljc3M1oifQ==" ... </code></pre> <p>其中:</p> <p><strong>X-Access-Token</strong>:Apache APISIX 将从用户提供商获取到的 access token 放入 X-Access-Token 请求头,可以通过插件配置中的 <code>access_token_in_authorization_header</code> 来选择是否放入 Authorization 请求头中。</p> <p><img src="https://static.apiseven.com/202108/1641278494765-139b6ffc-227b-4f02-8b2a-45d762422e15.png" alt="X-Access-Token" referrerpolicy="no-referrer"></p> <p><strong>X-Id-Token</strong>:Apache APISIX 将从用户提供商获取到的 ID token 通过 base64 编码之后放入 X-Id-Token 请求头,可以通过插件配置中的 <code>set_id_token_header</code> 来选择是否开启该功能,默认为为开启状态。</p> <p><img src="https://static.apiseven.com/202108/1641278494768-867dadf3-8ecd-4376-af03-d86b6a7aa698.png" alt="X-Id-Token" referrerpolicy="no-referrer"></p> <p><strong>X-Userinfo</strong>: Apache APISIX 将从用户提供商获取到的用户信息,通过 base64 编码之后放入 X-Userinfo,你可以通过插件配置中的 <code>set_userinfo_header</code> 来选择是否开启该功能,默认为开启状态。</p> <p><img src="https://static.apiseven.com/202108/1641278494771-42567d0c-8424-46e2-9c5b-a12cf1af6bc8.png" alt="X-Userinfo" referrerpolicy="no-referrer"></p> <p>由此可以看到,Apache APISIX 将会携带 <code>X-Access-Token</code>、<code>X-Id-Token</code> 和 <code>X-Userinfo</code> 三个请求头传递至上游。上游可以通过解析这几个头部,从而获取到用户 ID 信息和用户的元数据。</p> </li> <li> <p>在 Authing 控制台中的 「审计日志-用户行为日志」中可以观察到 user1 用户在 2021.12.27 17:54:17 成功登录。 <img src="https://static.apiseven.com/202108/1641276858261-00fb4d07-751e-42db-bbbf-e8550002466f.png" alt="登录信息" referrerpolicy="no-referrer"></p> </li> </ol> <h2>总结</h2> <p>本文为大家描述了 Apache APISIX 和 Authing 对接的详细操作步骤,通过阅读本文,大家对于在 Apache APISIX 中使用 Authing 有了更清晰的理解。</p> <p>Apache APISIX 不仅致力于保持自身的高性能,也一直非常重视开源生态的建设。目前 Apache APISIX 已经拥有了 10+ 个认证授权相关的插件,支持与业界主流的认证授权服务对接。</p> <p>如果你有对接其他认证授权的需求,不妨访问 Apache APISIX 的 <a href="https://github.com/apache/apisix/issues">GitHub</a>,通过 issue 留下你的建议;或订阅 Apache APISIX 的<a href="https://apisix.apache.org/zh/docs/general/subscribe-guide">邮件列表</a>,通过邮件表达你的想法。我们也会在每周三晚上 8:00 举行<a href="https://meeting.tencent.com/dm/VLjTH3CU3QVi">社区周会</a>,欢迎各位参加!</p>

再接再厉!Apache APISIX 集成 Open Policy Agent

<p>Open Policy Agent(OPA)是一个开源的轻量级通用策略引擎,可以代替软件中内置的策略功能模块,帮助用户实现服务与策略引擎的解耦。得益于 <a href="https://www.openpolicyagent.org/docs/latest/ecosystem/">OPA 完善的生态系统</a>,用户可以很容易地集成 OPA 和其他服务,例如程序库、HTTP API 等。</p> <p>如下图所示,OPA 首先通过策略语言 Rego 描述策略;然后通过 JSON 存储策略数据,之后用户就可以发送查询请求。收到查询请求后,OPA 将结合策略、数据和用户输入的查询请求内容生成策略决策,并将决策发送至服务。</p> <p><img src="https://static.apiseven.com/202108/1640332208554-40f574e3-0582-48f3-8e07-eb49fbd37ac7.png" alt="OPA Workflow" referrerpolicy="no-referrer"></p> <h2>插件介绍</h2> <p>Apache APISIX 提供了一个 <code>opa</code> 插件,用户可以使用这个插件,便捷地将 OPA 提供的策略能力引入到 Apache APISIX,实现灵活的身份认证与准入控制功能。</p> <p>将 <code>opa</code> 插件配置在路由上后,Apache APISIX 会在处理响应请求时,将请求信息、连接信息等组装成 JSON 数据,并将其发送到策略决策 API 地址。只要在 OPA 中部署的策略符合 Apache APISIX 设定的数据规范,就可以实现如通过请求、拒绝请求、自定义状态码、自定义响应头、自定义响应头等功能。</p> <p>本文以 HTTP API 为例为大家介绍 <code>opa</code> 插件,并详细说明如何将 Apache APISIX 与 OPA 进行集成,实现后端服务的认证授权解耦。</p> <h2>如何使用</h2> <h3>搭建测试环境</h3> <ol> <li> <p>使用 Docker 构建 OPA 服务。</p> <pre><code class="language-shell"># 使用 Docker 运行 OPA docker run -d --name opa -p 8181:8181 openpolicyagent/opa:0.35.0 run -s </code></pre> </li> <li> <p>创建 <code>example</code> 策略。</p> <pre><code class="language-shell"># 创建策略 curl -XPUT 'localhost:8181/v1/policies/example' \ --header 'Content-Type: text/plain' \ --data-raw 'package example import input.request import data.users default allow = false allow { # 具有名为 test-header 值为 only-for-test请求头 request.headers["test-header"] == "only-for-test" # 请求方法为 GET request.method == "GET" # 请求路径以 /get 开头 startswith(request.path, "/get") # GET 参数 test 存在且不等于 abcd request.query["test"] != "abcd" # GET 参数 user 存在 request.query["user"] } reason = users[request.query["user"]].reason { not allow request.query["user"] } headers = users[request.query["user"]].headers { not allow request.query["user"] } status_code = users[request.query["user"]].status_code { not allow request.query["user"] }' </code></pre> </li> <li> <p>创建 <code>users</code> 数据。</p> <pre><code class="language-shell"># 创建测试用户数据 curl -XPUT 'localhost:8181/v1/data/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "alice": { "headers": { "Location": "http://example.com/auth" }, "status_code": 302 }, "bob": { "headers": { "test": "abcd", "abce": "test" } }, "carla": { "reason": "Give you a string reason" }, "dylon": { "headers": { "Content-Type": "application/json" }, "reason": { "code": 40001, "desc": "Give you a object reason" } } }' </code></pre> </li> </ol> <h3>创建路由并启用插件</h3> <p>运行以下命令,创建路由,并启用 <code>opa</code> 插件。</p> <pre><code class="language-shell">curl -XPUT 'http://127.0.0.1:9080/apisix/admin/routes/r1' \ --header 'X-API-KEY: &lt;api-key&gt;' \ --header 'Content-Type: application/json' \ --data-raw '{ "uri": "/*", "methods": [ "GET", "POST", "PUT", "DELETE" ], "plugins": { "opa": { "host": "http://127.0.0.1:8181", "policy": "example" } }, "upstream": { "nodes": { "httpbin.org:80": 1 }, "type": "roundrobin" } }' </code></pre> <h3>测试请求</h3> <p>接下来,请运行以下命令,向 <code>opa</code> 插件发送请求,测试插件运行状态。</p> <pre><code class="language-shell"># 允许请求 curl -XGET '127.0.0.1:9080/get?test=none&amp;user=dylon' \ --header 'test-header: only-for-test' { "args": { "test": "abcd1", "user": "dylon" }, "headers": { "Test-Header": "only-for-test", "with": "more" }, "origin": "127.0.0.1", "url": "http://127.0.0.1/get?test=abcd1&amp;user=dylon" } # 拒绝请求并重写状态码和响应头 curl -XGET '127.0.0.1:9080/get?test=abcd&amp;user=alice' \ --header 'test-header: only-for-test' HTTP/1.1 302 Moved Temporarily Date: Mon, 20 Dec 2021 09:37:35 GMT Content-Type: text/html Content-Length: 142 Connection: keep-alive Location: http://example.com/auth Server: APISIX/2.11.0 # 拒绝请求并返回自定义响应头 curl -XGET '127.0.0.1:9080/get?test=abcd&amp;user=bob' \ --header 'test-header: only-for-test' HTTP/1.1 403 Forbidden Date: Mon, 20 Dec 2021 09:38:27 GMT Content-Type: text/html; charset=utf-8 Content-Length: 150 Connection: keep-alive abce: test test: abcd Server: APISIX/2.11.0 # 拒绝请求并返回自定义响应(字符串) curl -XGET '127.0.0.1:9080/get?test=abcd&amp;user=carla' \ --header 'test-header: only-for-test' HTTP/1.1 403 Forbidden Date: Mon, 20 Dec 2021 09:38:58 GMT Content-Type: text/plain; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Server: APISIX/2.11.0 Give you a string reason # 拒绝请求并返回自定义响应(JSON) curl -XGET '127.0.0.1:9080/get?test=abcd&amp;user=dylon' \ --header 'test-header: only-for-test' HTTP/1.1 403 Forbidden Date: Mon, 20 Dec 2021 09:42:12 GMT Content-Type: application/json Transfer-Encoding: chunked Connection: keep-alive Server: APISIX/2.11.0 {"code":40001,"desc":"Give you a object reason"} </code></pre> <h3>关闭插件</h3> <p>得益于 Apache APISIX 的动态化特性,只需要移除路由配置中 <code>opa</code> 插件相关配置并保存,即可关闭路由上的 OPA 插件。</p> <h2>总结</h2> <p>本文为大家描述了 Apache APISIX 和 Open Policy Agent 对接的详细操作步骤,希望通过本文可以让大家对于在 Apache APISIX 中使用 Open Policy Agent 有了更清晰的理解,方便后续进行上手实操。</p> <p>Apache APISIX 不仅致力于保持自身的高性能,也一直非常重视开源生态的建设。目前 Apache APISIX 已经拥有了 10+ 个认证授权相关的插件,支持与业界主流的认证授权服务对接。</p> <p>如果你有对接其他认证授权的需求,不妨访问 Apache APISIX 的 <a href="https://github.com/apache/apisix/issues">GitHub</a>,通过 issue 留下你的建议;或订阅 Apache APISIX 的<a href="https://apisix.apache.org/zh/docs/general/subscribe-guide">邮件列表</a>,通过邮件表达你的想法。</p>

生态丰富持续进行中,Apache OpenWhisk 集成闪亮登场

<p>本文将介绍 Apache APISIX 中新增插件——<code>openwhisk</code>,并通过详细步骤向大家展示如何将 OpenWhisk 服务与 Apache APISIX 进行集成,来享受无服务器计算的优势。此插件预计在 Apache APISIX 2.12 版本中正式上线,敬请期待!</p> <p><img src="https://static.apiseven.com/202108/1640313816872-b2c018be-5433-4baf-ba6a-8330e160866a.png" alt="APISIX&amp;OpenWhisk" referrerpolicy="no-referrer"></p> <h2>项目介绍</h2> <h3>Apache APISIX</h3> <p><a href="https://apisix.apache.org/">Apache APISIX</a> 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。Apache APISIX 不仅支持插件动态变更和热插拔,而且拥有众多实用的插件。</p> <h3>Apache OpenWhisk</h3> <p><a href="https://openwhisk.apache.org/">Apache OpenWhisk</a> 是一个开源的分布式无服务器平台,可以通过执行函数响应任何规模的时间。它使用 Docker 容器管理基础设施、服务器和规模扩展,助力用户构建出色且高效的应用程序。</p> <p>在 OpenWhisk 中开发者可以使用多种编程语言来编写函数(称为 Action),这些函数将由 OpenWhisk 负责动态调度并处理来自事件(通过 trigger)或外部请求(通过 HTTP 请求)的响应。</p> <h2>集成原理</h2> <p>Apache APISIX 为便捷集成 Apache OpenWhisk 提供了插件支持,用户可以定义一个包含无服务器插件的路由,并结合 Apache APISIX 提供的多种身份认证插件来实现认证与授权功能。</p> <p>大体操作原理如下:用户可以使用openwhisk插件在路由中定义一个“动态上游”,当路由匹配到请求时,它将中止到原上游的请求,并向 OpenWhisk 的 API Host 端点发送请求。</p> <p>请求中将包含用户为插件配置的 Namespace、Action、Service Token 以及原始 HTTP 请求体数据,并将从 OpenWhisk 中获取到的响应内容返回至客户端。</p> <h2>如何使用</h2> <h3>步骤一:搭建 Apache OpenWhisk 测试环境</h3> <ol> <li>首先,您需要确保使用 Linux 系统,并且系统中已经安装了 Docker 软件。执行以下命令:</li> </ol> <pre><code class="language-shell">docker run --rm -d \ -h openwhisk --name openwhisk \ -p 3233:3233 -p 3232:3232 \ -v /var/run/docker.sock:/var/run/docker.sock \ openwhisk/standalone:nightly docker exec openwhisk waitready </code></pre> <ol start="2"> <li>等待命令执行完成后,将输出以下内容:</li> </ol> <pre><code>ok: whisk auth set. Run 'wsk property get --auth' to see the new value. ok: whisk API host set to http://openwhisk:3233 ok: updated action testme server initializing... server initializing... "ready": true ok: deleted action testme </code></pre> <ol start="3"> <li>创建以下文件 <code>test.js</code> 用作测试函数。</li> </ol> <pre><code class="language-java">function main(args) { return { "hello": args.name || "", }; } </code></pre> <ol start="4"> <li>在 OpenWhisk 中注册以上函数。</li> </ol> <pre><code class="language-shell"># 为 OpenWhisk CLI 工具设置 API Host 和鉴权信息,您可以从这里 https://s.apache.org/openwhisk-cli-download 下载它 wsk property set \ --apihost 'http://localhost:3233' \ --auth '23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP' # 创建测试函数 wsk action create test test.js </code></pre> <h3>步骤二:创建路由并开启 <code>openwhisk</code> 插件</h3> <p>下面我们将创建一个路由,并为其添加 <code>openwhisk</code> 插件。执行以下命令:</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { "openwhisk": { "api_host": "http://localhost:3233", "service_token": "23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP", "namespace": "guest", "action": "test" } }, "uri": "/openwhisk" }' </code></pre> <h3>步骤三:测试请求</h3> <p>下面我们将使用 cURL 进行测试。</p> <pre><code class="language-shell"># 使用 POST 请求并发送数据 curl http://127.0.0.1:9080/openwhisk -i -X POST -d '{ "name": "world" }' HTTP/1.1 200 OK Content-Type: application/json Content-Length: 17 Server: APISIX/2.10.2 {"hello":"world"} # 使用 GET 请求 curl http://127.0.0.1:9080/openwhisk -i HTTP/1.1 200 OK Content-Type: application/json Content-Length: 12 Server: APISIX/2.10.2 {"hello":""} </code></pre> <h3>步骤四:测试复杂响应</h3> <ol> <li>创建并更新 <code>test.js</code> 测试函数</li> </ol> <pre><code class="language-java">function main(args) { return { "status": "403", "headers": { "test": "header" }, "body": "A test body" }; } 2. 进行测试请求 ```shell # 使用 GET 请求 curl http://127.0.0.1:9080/openwhisk -i HTTP/1.1 403 FORBIDDEN Content-Type: application/json Content-Length: 12 test: header Server: APISIX/2.10.2 A test body </code></pre> <h3>补充:关闭插件</h3> <p>如使用完毕,只需移除路由配置中 OpenWhisk 相关配置并保存,即可关闭路由上的 OpenWhisk 插件。此时您可以开启其他 Serverless 类插件或添加上游等后续操作。</p> <p>得益于 Apache APISIX 的动态化优势,开启关闭插件的过程都不需要重启 Apache APISIX,十分方便。</p> <h2>总结</h2> <p>本文为大家介绍了关于 <code>openwhisk</code> 插件的功能前瞻与使用步骤,更多关于 <code>openwhisk</code> 插件说明和完整配置列表,可以参考<a href="https://apisix.apache.org/docs/apisix/next/plugins/openwhisk">官方文档</a>。</p> <p>目前,我们也在开发其他 Serverless 插件以便与更多云服务进行集成。如果您对此类集成项目感兴趣,也欢迎随时在 <a href="https://github.com/apache/apisix/discussions">GitHub Discussions</a> 中发起讨论,或通过<a href="https://apisix.apache.org/zh/docs/general/subscribe-guide">邮件列表</a>进行交流。</p>

捷报频传!Apache APISIX 现已支持对接 Google Cloud Logging

<p><img src="https://static.apiseven.com/202108/1640155567091-2611f8b8-8181-42d8-8756-e892b3768a8d.png" alt="Apache APISIX-Google Cloud logging cover" referrerpolicy="no-referrer"></p> <p>日志是大型分布式系统的重要基础设施,可以帮助开发者检查观测服务运行的状态,提高服务故障排查和诊断效率以及进行多维度的分析,以此提高系统整体的稳定性和运行效率。</p> <p>Google Cloud Logging 是由 Google Cloud 提供的全代管式实时日志管理服务,提供 EB 级的存储、搜索、分析和提醒等服务。通过 Google Cloud Loging 的日志浏览器你可以简单高效的对日志进行对日志进行搜索、排序和分析,并且 Google Cloud Logging 还提供了保存查询和丰富的图表功能可以使日志筛查结果可回溯且有更直观的呈现。</p> <p>Apache APISIX 在此之前已经支持集成了 <a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/http-logger.md">HTTP Logger</a> 、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/tcp-logger.md">TCP Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/kafka-logger.md">Kafka Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/udp-logger.md">UDP Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/rocketmq-logger.md">RocketMQ Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/skywalking-logger.md">SkyWalking Logger</a>、<a href="https://github.com/apache/apisix/blob/master/docs/en/latest/plugins/sls-logger.md">Aliyun Cloud Logging(SLS)</a>等众多开源及云日志服务解决方案。</p> <p>最近,Apache APISIX 对 Google Cloud Logging 也完成了支持,在使用 Apache APISIX 作为网关时用户又多了一种新的日志解决方案:使用 google-cloud-logging 插件,将 Apache APISIX 的请求日志转发到 Google Cloud Logging 服务中进行分析和存储。</p> <p>启用该插件后, Apache APISIX 将在 Log Phase 获取请求上下文信息并序列化为 Google Cloud Logging 的日志格式,然后将序列化后的日志数据提交到批处理队列中,当批处理队列触发用户设置的时间或条目阈值时会将日志数据通过 Google Cloud API 批量转发到 Google Cloud Logging 服务中。</p> <p>本文将为大家介绍如何在 Apache APISIX 中配置和使用 Google Cloud Logging 服务。</p> <h2>配置 Google Cloud</h2> <ol> <li>打开浏览器,访问 Google Cloud 首页。</li> <li>输入用户名和密码,登录 Google Cloud 控制台。</li> <li>单击 Google Cloud 控制台左侧菜单,选择 “IAM &amp; Admin &gt; Create a Project”,开始创建项目。 <img src="https://static.apiseven.com/202108/1640137078950-3a0b472b-df9f-4f75-9c03-816138860f74.png" alt="创建项目" referrerpolicy="no-referrer"></li> <li>输入项目名称,选择组织名称,单击 “CREATE” 创建项目。 <img src="https://static.apiseven.com/202108/1640137136967-effec599-2263-45e7-874d-53a547b83aae.png" alt="创建项目-2" referrerpolicy="no-referrer"></li> <li>创建项目成功后,控制台右上角提示创建成功。 <img src="https://static.apiseven.com/202108/1640137177601-6ac703ef-99e4-4ac2-82e3-5b978348f458.png" alt="成功创建项目" referrerpolicy="no-referrer"></li> <li>在窗口中点击选择项目,或在控制台首页顶部导航栏选择项目操作路径。选择项目后,将跳转至控制台首页,此时在顶部导航栏和信息中心的项目信息中已经可以看到当前项目的相关数据。 <img src="https://static.apiseven.com/202108/1640137215687-4a2a4789-09d3-4cc0-85fa-be67762cf9b7.png" alt="查看项目" referrerpolicy="no-referrer"></li> <li>完成项目创建后,你需要为该项目创建服务账号。请返回 Google Cloud 控制台首页,单击左侧菜单“IAM &amp; Admin &gt; Service Account”,开始创建服务账号。 <img src="https://static.apiseven.com/202108/1640137733012-6c9808c8-9c96-401e-a680-03a276b964c0.png" alt="开始创建服务账号" referrerpolicy="no-referrer"></li> <li>单击“CREATE SERVICE ACCOUNT”创建服务账号。 <img src="https://static.apiseven.com/202108/1640137784375-e47cbe0e-7735-4e7b-a881-1a9ec1c12ffc.png" alt="创建服务账号" referrerpolicy="no-referrer"></li> <li>输入服务账号名称及 ID(ID 一般跟随账号生成),然后单击 “CREATE AND CONTINUE”。 <img src="https://static.apiseven.com/202108/1640137834702-76166e6f-ed98-4a85-a759-2ce78f795794.png" alt="创建服务账号-2" referrerpolicy="no-referrer"></li> <li>单击“Role”,在搜索框中输入“Logging Admin”搜索这个角色,选择“Logging Admin”作为角色。 <img src="https://static.apiseven.com/202108/1640137883981-0f780040-8398-4d38-9600-a5e54b29b48e.png" alt="创建服务账号-3" referrerpolicy="no-referrer"></li> <li>单击“DONE”,完成服务账号创建,跳转到服务账号首页。此时你可以在列表中看到刚刚创建的账号及详情。 <img src="https://static.apiseven.com/202108/1640137970837-ed1994be-87d0-48b8-bec5-010200fe1f1d.png" alt="查看账号及详情" referrerpolicy="no-referrer"></li> <li>在服务账号最后一列的操作栏单击“Manage keys”,进入秘钥管理界面。 <img src="https://static.apiseven.com/202108/1640138660649-cd57da29-5965-4251-9deb-300de830dfd9.png" alt="进入秘钥管理界面" referrerpolicy="no-referrer"></li> <li>单击“ADD KEY &gt; Create new key”,开始创建新秘钥。 <img src="https://static.apiseven.com/202108/1640138732589-1aea201b-de2d-455a-8c04-c3f5a28dfa91.png" alt="创建新秘钥" referrerpolicy="no-referrer"></li> <li>在弹窗页中选择秘钥类型为“JSON”,然后单击“CREATE”,创建新秘钥。 <img src="https://static.apiseven.com/202108/1640138785425-23ee8efe-bc0d-428a-a627-2f428440da37.png" alt="创建新秘钥-2" referrerpolicy="no-referrer"></li> <li>私钥信息将通过浏览器自动下载到系统默认 Downloads 目录中,启用 google-cloud-logging 插件时,需要使用这个私钥中的信息,因此请妥善保存私钥文件。 <img src="https://static.apiseven.com/202108/1640138820163-aa459874-e78e-4156-ab74-58fc7e2ae13f.png" alt="下载私钥文件" referrerpolicy="no-referrer"></li> </ol> <h2>配置 Apache APISIX</h2> <h3>启用 google-cloud-logging 插件</h3> <h4>方式一:上传私钥文件配置</h4> <ol> <li>将私钥文件上传到 Apache APISIX 节点服务器中。</li> <li>将文件路径配置到 <code>google-cloud-logging. auth_file</code> 配置项上,如下所示:</li> </ol> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri":"/logging.do", "upstream":{ "type":"roundrobin", "nodes":{ "127.0.0.1:1980":1 } }, "plugins":{ "google-cloud-logging":{ // Google Cloud Logging 私钥文件 "auth_file":"/path/to/apache-apisix-fcafc68c2f41.json", // 每个批处理队列最大容纳日志条目数 "batch_max_size": 1, // 刷新批处理队列缓冲区的最大时间(以秒为单位) "inactive_timeout": 10 } } }' </code></pre> <h4>通过 JSON 文本配置</h4> <ol> <li>打开私钥文件。</li> <li>将 <code>project_id</code> 的值配置到 <code>google-cloud-logging. auth_config.project_id</code> 配置项中。</li> <li>将 <code>private_key</code> 的值配置到 <code>google-cloud-logging. auth_config. private_key</code> 配置项中。</li> </ol> <p>如下所示:</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri":"/logging.do", "upstream":{ "type":"roundrobin", "nodes":{ "127.0.0.1:1980":1 } }, "plugins":{ "google-cloud-logging":{ // Google Cloud Logging 私钥文件 "auth_config":{ "project_id":"apache-apisix", "private_key":"-----BEGIN RSA PRIVATE KEY-----your private key-----END RSA PRIVATE KEY-----" }, // 每个批处理队列最大容纳日志条目数 "batch_max_size": 1, // 刷新批处理队列缓冲区的最大时间(以秒为单位) "inactive_timeout": 10 } } }' </code></pre> <h4>参数说明</h4> <table> <thead> <tr> <th style="text-align:left">参数名称</th> <th style="text-align:left">是否必填</th> <th style="text-align:left">默认值</th> <th style="text-align:left">描述</th> </tr> </thead> <tbody> <tr> <td style="text-align:left">auth_config</td> <td style="text-align:left">否</td> <td style="text-align:left">n/a</td> <td style="text-align:left">Google Cloud Logging 私钥文件,必须配置 auth_config 或 auth_file 之一</td> </tr> <tr> <td style="text-align:left">auth_config.private_key</td> <td style="text-align:left">是</td> <td style="text-align:left">n/a</td> <td style="text-align:left">Google Cloud Logging 的私钥参数</td> </tr> <tr> <td style="text-align:left">auth_config.project_id</td> <td style="text-align:left">是</td> <td style="text-align:left">n/a</td> <td style="text-align:left">谷歌服务帐号的项目 ID</td> </tr> <tr> <td style="text-align:left">auth_config.token_uri</td> <td style="text-align:left">否</td> <td style="text-align:left">oauth2.googleapis.com/token</td> <td style="text-align:left">请求 Google Service Account 的令牌的 URI</td> </tr> <tr> <td style="text-align:left">auth_config.entries_uri</td> <td style="text-align:left">否</td> <td style="text-align:left">logging.googleapis.com/v2/entries:write</td> <td style="text-align:left">Google Cloud Logging 写入日志条目的 API</td> </tr> <tr> <td style="text-align:left">auth_config.scopes</td> <td style="text-align:left">否</td> <td style="text-align:left">["https://www.googleapis.com/auth/logging.read","https://www.googleapis.com/auth/logging.write","https://www.googleapis.com/auth/logging.admin","https://www.googleapis.com/auth/cloud-platform"]</td> <td style="text-align:left">谷歌服务账号的访问范围, 参考:<a href="https://developers.google.com/identity/protocols/oauth2/scopes#logging">OAuth 2.0 Scopes for Google APIs</a></td> </tr> <tr> <td style="text-align:left">auth_file</td> <td style="text-align:left">否</td> <td style="text-align:left">n/a</td> <td style="text-align:left">谷歌服务账号 JSON 文件的路径(必须配置 auth_config 或 auth_file 之一)</td> </tr> <tr> <td style="text-align:left">ssl_verify</td> <td style="text-align:left">否</td> <td style="text-align:left">TRUE</td> <td style="text-align:left">启用 SSL 验证, 配置根据 <a href="https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake">OpenResty documentation</a>选项</td> </tr> <tr> <td style="text-align:left">resource</td> <td style="text-align:left">否</td> <td style="text-align:left">{"type": "global"}</td> <td style="text-align:left">谷歌监控资源,参考:<a href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource">MonitoredResource</a></td> </tr> <tr> <td style="text-align:left">log_id</td> <td style="text-align:left">否</td> <td style="text-align:left">apisix.apache.org%2Flogs</td> <td style="text-align:left">谷歌日志 ID,参考:<a href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry">LogEntry</a>.</td> </tr> <tr> <td style="text-align:left">max_retry_count</td> <td style="text-align:left">否</td> <td style="text-align:left">0</td> <td style="text-align:left">从处理管道中移除之前的最大重试次数</td> </tr> <tr> <td style="text-align:left">retry_delay</td> <td style="text-align:left">否</td> <td style="text-align:left">1</td> <td style="text-align:left">如果执行失败,流程执行应延迟的秒数</td> </tr> <tr> <td style="text-align:left">buffer_duration</td> <td style="text-align:left">否</td> <td style="text-align:left">60</td> <td style="text-align:left">必须先处理批次中最旧条目的最大期限(以秒为单位)</td> </tr> <tr> <td style="text-align:left">inactive_timeout</td> <td style="text-align:left">否</td> <td style="text-align:left">10</td> <td style="text-align:left">刷新缓冲区的最大时间(以秒为单位)</td> </tr> <tr> <td style="text-align:left">batch_max_size</td> <td style="text-align:left">否</td> <td style="text-align:left">100</td> <td style="text-align:left">每个批处理队列可容纳的最大条目数</td> </tr> </tbody> </table> <h3>验证插件是否成功运行</h3> <ol> <li>运行以下命令,向 Google Cloud Logging 发送请求。</li> </ol> <pre><code class="language-shell">curl -i http://127.0.0.1:9080/logging.do HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Date: Fri, 10 Dec 2021 09:57:52 GMT Server: APISIX/2.11.0 Hello, Google Cloud Logging </code></pre> <ol start="2"> <li>打开浏览器,访问 Google Cloud 首页。</li> <li>输入用户名和密码,登录 Google Cloud 控制台。</li> <li>通过日志浏览器查看发送的请求日志,返回结果如下图所示。 <img src="https://static.apiseven.com/202108/1640139014263-fac86f87-d008-475c-aeae-289ab4ba62a8.png" alt="查看日志" referrerpolicy="no-referrer"></li> </ol> <h3>停用 google-cloud-logging 插件</h3> <p>如使用结束,可以移除 <code>google-cloud-logging</code> 相关配置块进行停用该插件。</p> <pre><code class="language-shell">curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri":"/logging.do", "upstream":{ "type":"roundrobin", "nodes":{ "127.0.0.1:1980":1 } }, "plugins":{ } }' </code></pre> <h2>总结</h2> <p>本文为大家描述了 Apache APISIX 和 Google Cloud Logging 对接的详细操作步骤,希望通过本文可以让大家对于在 Apache APISIX 中使用 Google Cloud Logging 有更清晰的理解,方便后续进行上手实操。</p> <p>Apache APISIX 不仅致力于保持自身的高性能,也一直非常重视开源生态的建设。目前 Apache APISIX 已经拥有了 10+ 个日志相关的插件,支持与业界主流的开源日志项目对接。</p> <p>如果你有对接其他日志的需求,不妨访问 Apache APISIX 的 <a href="https://github.com/apache/apisix/issues">GitHub</a>,通过 issue 留下你的建议;或订阅 Apache APISIX 的<a href="https://apisix.apache.org/zh/docs/general/subscribe-guide">邮件列表</a>,通过邮件表达你的想法。</p> <h2>相关阅读</h2> <p><a href="https://apisix.apache.org/zh/blog/2021/12/07/apisix-integrate-skywalking-plugin/">强强联合!APISIX 集成 SkyWalking 打造全方位日志处理</a></p> <p><a href="https://apisix.apache.org/zh/blog/2021/12/08/apisix-integrate-rocketmq-logger-plugin/">Apache APISIX 携手 RocketMQ 为实时 API 日志监控功能再下一城</a></p>

两种方式教你在 K8s 中轻松部署 Apache APISIX

<p>Apache APISIX 是一个动态、实时、高性能的开源 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。</p> <p>而 Kubernetes 作为自动部署、扩展和管理容器化应用程序的开源系统,旨在为用户提供<strong>跨主机集群</strong>的自动部署、扩展以及运行应用程序容器等相关功能支持。如何快速地在 K8s 中部署 Apache APISIX 并通过 Dashboard 进行相关信息的展示,这里我们为大家整理了两种非常容易上手的安装思路。</p> <h2>环境准备</h2> <p>在正式进入安装部署前,确保网络畅通并准备好相应的 K8s 集群。</p> <p>在这里,我们推荐使用 Kind 进行本地搭建 K8s 集群测试环境,非常方便且容易上手。根据<a href="https://kind.sigs.k8s.io/docs/user/quick-start/">官方文档</a>步骤安装好 Kind 后,只需一条指令即可搭建好 K8s 集群环境。</p> <pre><code class="language-shell">kind create cluster </code></pre> <h2>方式一:通过 Helm 安装</h2> <p>Helm 主要用于管理 Kubernetes 中的应用程序。类似 apt、yum、pacman 这些 Linux 中的包管理器,所以 Helm 也称为 Kubernetes 中的包管理器。</p> <p>目前 Apache APISIX 已经提供了 <a href="https://github.com/apache/apisix-helm-chart">Helm Chart 仓库</a>,用户可以非常方便地通过 Helm 来部署和卸载 Apache APISIX。</p> <h3>部署 Apache APISIX</h3> <p>首先添加 Apache APISIX Helm Chart 地址并更新仓库。</p> <pre><code class="language-shell">helm repo add apisix https://charts.apiseven.com helm repo update </code></pre> <p>进行安装 Apache APISIX(此处演示是将 Apache APISIX 安装到 Default Namespace,如需自定义 Namespace,可参考<a href="https://kubernetes.io/docs/tasks/administer-cluster/namespaces/#creating-a-new-namespace">相关文档</a>)。</p> <pre><code class="language-shell">helm install apisix apisix/apisix </code></pre> <p>上述指令执行成功后,会得到如下返回信息:</p> <pre><code class="language-shell">▶ helm install apisix apisix/apisix NAME: apisix LAST DEPLOYED: Sun Dec 5 14:43:19 2021 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: 1. Get the application URL by running these commands: export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services apisix-gateway) export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT </code></pre> <p>通过上述方式安装部署的 Apache APISIX,Admin API 会暴露在集群中 <code>9180</code> 端口,Gateway 暴露在 <code>80</code> 端口。如果要访问 Admin API,可以使用 <code>kubectl port-forward</code> 将端口转发到本地主机上的端口。</p> <p>这里演示的是转发到本机 <code>9080</code> 端口情况,主要是为了和 Apache APISIX 官方文档同步,方便后续验证。</p> <pre><code class="language-shell">kubectl port-forward service/apisix-admin 9080:9180 </code></pre> <p>之后可参考 <a href="https://apisix.apache.org/zh/docs/apisix/getting-started/">Apache APISIX 快速入门指南</a>,进行 Upstream 的添加与绑定 Route 等相关操作。</p> <p>最后进行新建路由的验证环节。</p> <p>由于在本文演示中使用了 Kind 来搭建本地 K8s 集群,<code>apisix-gateway</code> 的 NodePort 访问不了,所以在验证之前还需要额外一步,即将集群中的 <code>80</code> 端口转发到本机 <code>8080</code> 端口。</p> <pre><code class="language-shell">kubectl port-forward service/apisix-gateway 8080:80 </code></pre> <p>开始进行验证。</p> <pre><code class="language-shell">curl -X GET "http://127.0.0.1:8080/get?foo1=bar1&amp;foo2=bar2" -H "Host: httpbin.org" </code></pre> <p>期望的返回结果可以参考下方示例。</p> <pre><code class="language-json">{ "args": { "foo1": "bar1", "foo2": "bar2" }, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip", "Host": "httpbin.org", "User-Agent": "curl/7.64.1", "X-Amzn-Trace-Id": "Root=1-61ac63b5-348d3c5567db393462cd0666", "X-Forwarded-Host": "httpbin.org" }, "origin": "127.0.0.1, 192.46.208.201", "url": "http://httpbin.org/get?foo1=bar1&amp;foo2=bar2" } </code></pre> <h3>部署 Apache APISIX-Dashboard</h3> <p>与部署 Apache APISIX 一样,通过 Helm 安装 Apache APISIX-Dashboard 也只需执行一条指令。</p> <pre><code class="language-shell">helm install apisix-dashboard apisix/apisix-dashboard </code></pre> <p>接下来转发 Dashboard 端口到本机。</p> <pre><code class="language-shell">kubectl port-forward service/apisix-dashboard 8080:80 </code></pre> <p>最后访问本地 <code>localhost:8080</code> 就可以看到登录页面了。</p> <p>注意:这里部署出来的 Apache APISIX-Dashboard 系统信息中不会出现 Apache APISIX 的节点信息。因为通过 Helm 方式进行安装的话默认是没有启用 <code>server-info</code> 插件,如有需要可在 apisix 的configmap 中自行添加。<code>server-info</code> 的配置可参考<a href="https://apisix.apache.org/docs/apisix/plugins/server-info/">相关文档</a>。</p> <h2>方式二:通过 yaml 文件部署</h2> <p>相较于上述 Helm 方式的部署,使用 yaml 文件来部署 Apache APISIX,可以更加方便地进行自定义配置。</p> <h3>部署 APISIX 及 Dashboard</h3> <p>注意:如果已经使用方式一部署过,在进行下述安装前需要先清除 ETCD 的 PVC 存储以方便后续操作。</p> <p>这里我们已经整理好了在部署 Apache APISIX、APISIX-Dashboard 及 etcd 集群时需要用到的 yaml 文件,大家可通过 <a href="https://github.com/zaunist/apisix-on-kubernetes">apisix-on-kubernetes 仓库</a>进行调用接下来提到的相关文件。</p> <p>首先克隆上述提到的 <code>apisix-on-kubernetes</code> 仓库。</p> <pre><code class="language-shell">git clone https://github.com/zaunist/apisix-on-kubernetes.git </code></pre> <p>然后执行以下命令。</p> <pre><code class="language-shell">kubectl apply -f etcd.yaml kubectl apply -f apisix.yaml kubectl apply -f apisix-dashboard.yaml </code></pre> <p>等待 Pod 全部启动,将 <code>apisix-dashboard</code> 端口转发到本机。</p> <pre><code class="language-shell">kubectl port-forward service/apisix-dashboard 8080:80 </code></pre> <p>最后访问 <code>localhost:8080</code>,即可查看 Dashboard 相关信息。默认登录账号密码为 <code>admin</code>、<code>admin</code>。</p> <p>提示:在安装过程中为了更直观地看到部署情况,可以尝试利用 <a href="https://github.com/kubernetes/dashboard">Kubernetes Dashboard</a> 在网页端查看 Pod 的运行情况。</p> <h2>总结</h2> <p>本文介绍了两种在 Kubernetes 中部署 Apache APISIX 和 Apache APISIX-Dashboard 的方式。两种方式的最终目的都是一样的,但在使用过程中也各有各的优势。</p> <p>比如使用 Helm 进行安装非常方便,仅需几条指令就可执行所有操作;通过 YAML 文件部署则比较方便地进行相关自定义配置的修改,更具有可操控性。</p> <p>具体在实际场景中如何进行 Apache APISIX 的安装与部署,还要看大家的使用习惯,这里也仅为大家提供了两种思路进行参考。希望各位在后续使用 Apache APISIX 过程中可以开发出更多有趣的技巧和方法。</p>

使用 Prometheus 监控 APISIX Ingress Controller

<p>无论是之前单体应用的时代,或是云原生大行其道的今天,「监控功能」一直扮演着十分重要的角色。一个良好的监控体系能够帮助工程师们快速了解运行在生产环境中的服务状态,并在服务异常时快速定位问题或者提前预警异常的发生。</p> <p>Apache APISIX Ingress Controller 在最近几个版本中加强了对 Prometheus Metrics 的支持。本文将为大家介绍如何使用 Prometheus 收集 APISIX Ingress Controller 的 Metrics 数据,并通过 Grafana 进行后续可视化呈现。</p> <h2>步骤一:安装 APISIX Ingress Controller</h2> <p>首先我们通过 <a href="https://helm.sh/">Helm</a> 将 Apache APISIX、ETCD 和 APISIX Ingress Controller 部署到本地的 Kubernetes 集群中。</p> <pre><code class="language-shell">helm repo add apisix https://charts.apiseven.com helm repo update kubectl create namespace ingress-apisix helm install apisix apisix/apisix --namespace ingress-apisix \ --set ingress-controller.enabled=true </code></pre> <p>安装完毕后请等待,直至所有服务都已正常运行。具体状态确认可通过下述命令进行检查。</p> <pre><code class="language-shell">kubectl get all -n ingress-apisix </code></pre> <h2>步骤二:启用 Prometheus 插件</h2> <p>大部分情况下,监控功能的涉及面肯定不止 APISIX Ingress Controller 这一个组件,如果需要同时监控 Apache APISIX,可以创建以下 <code>ApisixClusterConfig</code> 资源。</p> <pre><code class="language-yaml">apiVersion: apisix.apache.org/v2alpha1 kind: ApisixClusterConfig metadata: name: default spec: monitoring: prometheus: enable: true </code></pre> <h3>安装 Prometheus 和 Grafana</h3> <p>接下来我们将通过 Prometheus Operator 来启用 Prometheus 服务,因此需要先安装 Prometheus Operator(下述命令同时也会安装 Grafana)。</p> <pre><code class="language-shell">helm repo add prometheus-community https://prometheus-community.github.io/helm-charts heml repo update kubectl create namespace prometheus helm install prometheus prometheus-community/kube-prometheus-stack -n prometheus </code></pre> <p>安装完毕后,需要准备 Prometheus 实例的 RBAC 配置。该配置可赋予 Prometheus 实例向 Kubernetes API Server 获取 Pod 和 Service 资源的能力。</p> <pre><code class="language-yaml">apiVersion: v1 kind: ServiceAccount metadata: name: ingress-apisix namespace: ingress-apisix --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: ingress-apisix rules: - apiGroups: [""] resources: - nodes - nodes/metrics - services - endpoints - pods verbs: ["get", "list", "watch"] - apiGroups: [""] resources: - configmaps verbs: ["get"] - apiGroups: - networking.k8s.io resources: - ingresses verbs: ["get", "list", "watch"] - nonResourceURLs: ["/metrics"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: ingress-apisix roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: ingress-apisix subjects: - kind: ServiceAccount name: ingress-apisix namespace: ingress-apisix </code></pre> <p>完成上述实例配置后,需要定义 PodMonitor,也可以根据场景需求选择使用 ServiceMonior。下述 PodMonitor 资源将聚焦于 APISIX Ingress Controller Pod 的 Metrics 收集。</p> <pre><code class="language-yaml">apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: name: ingress-apisix namespace: ingress-apisix labels: use-for: ingress-apisix spec: selector: matchLabels: app.kubernetes.io/name: ingress-controller podMetricsEndpoints: - port: http </code></pre> <blockquote> <p>这里不使用 ServiceMonitor 的原因是 <code>http</code> 这一端口没有暴露到 Service 级别。</p> </blockquote> <p>最后可通过下述指令进行 Prometheus 实例的定义。</p> <pre><code class="language-yaml">apiVersion: monitoring.coreos.com/v1 kind: Prometheus metadata: name: ingress-apisix namespace: ingress-apisix spec: serviceAccountName: ingress-apisix podMonitorSelector: matchLabels: use-for: ingress-apisix resources: requests: memory: 400Mi enableAdminAPI: false image: prom/prometheus:v2.31.0 </code></pre> <p>将上述资源全部应用到 Kubernetes 集群后,等待相关组件就绪。</p> <h2>步骤三:配置 Grafana</h2> <p>接下来我们通过配置 Grafana 进行可视化呈现。</p> <p>首先访问 <code>prometheus-grafana</code> 服务。注意,如果没有暴露 Service 到集群外部的手段,可以尝试使用端口转发的方式,Grafana 的管理员用户名和密码保存在 <code>prometheus-grafana</code> 这一 Secret 中。</p> <p>打开 Grafana 后,导入 APISIX Ingress Controller 的 <a href="https://raw.githubusercontent.com/apache/apisix-ingress-controller/22e548bc267115ccd36aec4200d5399aab565958/docs/assets/other/json/apisix-ingress-controller-grafana.json">Dashboard 模版</a>即可看到监控大盘,效果图如下:</p> <p><img src="https://static.apiseven.com/202108/1639381275740-d9e3b2a7-6895-43f2-8119-212ea616dddd.png" alt="效果图1" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/202108/1639381348652-7fb30365-179c-4b68-a168-ec3c9da7324d.png" alt="效果图2" referrerpolicy="no-referrer"></p> <p><img src="https://static.apiseven.com/202108/1639381376926-d6af92c7-16dd-4306-8931-9b83e7e8dce1.png" alt="效果图3" referrerpolicy="no-referrer"> 创建 Dashboard 模版可参考<a href="https://github.com/apache/apisix-ingress-controller/pull/731">相关 PR</a>。</p> <h2>补充:监控指标释义</h2> <p>目前 APISIX Ingress Controller 的监控指标聚焦于它和数据面 Apache APISIX 实例的交互(配置下发)上,包括配置同步的数量与延迟等相关数据。</p> <ul> <li><code>is_leader</code>:当前 APISIX Ingress Controller 实例是否为 Leader 角色。同一组 APISIX Ingress Controller 只会有一个 Leader,其余实例均为 Candidate。</li> <li><code>sync_operations</code>:包括 APISIX Ingress Controller 向数据面同步配置时的一些指标,包括推送次数、失败率、延迟等。通过这类指标可以监控配置下发是否正常,以此来帮助研发和运维同学进行监控预警和故障定位。</li> </ul> <h2>总结</h2> <p>本文介绍了如何使用 Prometheus 监控 APISIX Ingress Controller 的相关步骤和部分指标的展示效果。目前只包含了一些基本的监控指标,我们会在后续继续进行打磨和升级,增加更多的指标项和集成数据面 APISIX 的指标,为大家带来更好的监控体验。</p> <p>当然也欢迎感兴趣的朋友参与 <a href="https://github.com/apache/apisix-ingress-controller">Apache APISIX Ingress Controller 项目</a>的代码贡献中来,期待我们一起将 APISIX Ingress Controller 打造地更加全面化。</p>