本文根据 2024 年 5 月 25 日在上海举办的“云原生✖️AI 时代的微服务架构与技术实践”CloudWeGo 技术沙龙上海站活动中,Construct 服务端总监 Jason 的演讲《从 0 到 1 基于 Kitex + Istio 的微服务系统建设》整理而来。
在微服务架构的浪潮中,企业面临着从单体应用到分布式系统的转型挑战。本文以 Construct 公司为例,探讨其如何利用 CloudWeGo 框架和 Istio 服务网格,从零开始构建起一个高效、稳定的微服务系统。
关于 Construct
Construct Tech(肯斯爪特),成立于 2019 年,目前旗下 Litmatch 等产品,已覆盖海外十几个国家,并处于各国社交榜前 10 的位置,每月数千万用户使用。
一、从 0 到 1 的微服务系统建设
服务端架构的演进反映了互联网业务从简单到复杂的发展需求。Construct 公司见证了从单体架构,再到微服务架构的服务注册与发现机制,最终到服务网格(Service Mesh)的演进。
在互联网的早期阶段,应用相对简单,许多人可能还记得那个时代——个人站长主导的时期。那时的系统架构具有一种烟囱式结构,这种结构从上至下垂直延伸,简单直观,易于理解和使用。数据共享主要通过底层数据库实现。
随着业务需求的增长和互联网的快速发展,传统的烟囱式架构开始显得力不从心。为了解决这个问题,SOA 架构(Service Oriented Architecture)应运而生。SOA 引入了抽象层,以促进服务的更好复用,核心围绕着服务的解耦和服务总线的概念。服务总线作为通信的中介,使得服务调用方和服务提供方能够相互独立。然而,随着业务量的激增,服务总线逐渐成为系统的性能瓶颈。例如,当每秒查询率(QPS)飙升,用户数量大幅增长时,服务总线的任何故障都可能导致整个服务系统的瘫痪,这无疑是一场噩梦。
为了应对这一挑战,微服务架构应运而生。它进一步细化了服务的拆分,摒弃了中心化服务总线的概念,转而采用服务注册和发现机制来管理服务间的调用。这样,服务提供者和消费者可以通过注册中心相互发现并建立联系,有效解决了依赖问题。
在微服务架构的基础上,进一步发展出了服务网格(Service Mesh)架构,例如基于 Istio 的实现。服务网格进一步抽象了服务间的通信、注册和依赖关系,实现了语言无关性,使得不同编程语言编写的服务能够在同一架构下无缝交互和通信。这种解耦合的方法为多语言环境下的企业提供了更大的灵活性和扩展性。
作为一家拥有多款海外社交产品的公司,Construct 公司需要处理数千万月活用户的数据。面对业务的全球化和复杂化,公司决定采用微服务架构来提升系统的灵活性和可维护性。
今天,我们主要讨论的是我们公司从单体架构向服务网格(Service Mesh)架构的转型,这一转型是基于 Istio 实现的。接下来,我将分享我们转型时面临的背景和约束条件。
在我们着手构建新的微服务架构之前,公司已经拥有了一个基于 Python 的成熟单体架构。面对这一现实,我们需要解决的一个关键问题是如何确保新系统与现有系统的无缝交互和兼容性。这不仅是一个技术挑战,更是我们必须达成的硬性目标。通过深入分析,我们决定采用单向依赖的方法来简化集成过程。在接下来的架构演示中,我将详细阐述这一点。
接下来是我们的技术选型问题。面对众多的微服务框架,我们经过深思熟虑,最终将选择范围缩小到了两种:广泛使用的 gRPC 和新兴的 Kitex。尽管 gRPC 因其流行度和长时间的市场验证而备受关注,但我们最终选择了 Kitex。这一决策基于几个核心因素:Kitex 的易用性、高性能,尤其是在高并发环境下的出色表现。我们的基准测试显示,Kitex 的 QPS 是 gRPC 的两倍,而且在延迟方面表现更为优异。这些优势使得 Kitex 成为了我们的首选。
此外,考虑到我们需要构建的是基于服务网格的系统,我们必须确保所选技术能够与 Istio 兼容。当时,尽管缺乏现成的集成示例,并且我们是较早采用 Kitex 的公司之一,我们依然决定迎难而上。在 CloudWeGo 社区的大力支持下,我们不仅理解了 Kitex 与 Istio 集成的深层逻辑,还成功地将它们整合在一起。
我们通过逻辑分析和实践探索,证明了即使在当时没有示例的情况下,通过深入理解技术原理和逻辑,依然可以成功实现技术集成。我们选择的是 Kitex 的 gRPC 协议,因为它提供了高效的通信机制,并且与 Istio 有着良好的互操作性。
在集成过程中,我们遇到了一些关键技术点和挑战。在服务器端,我们特别关注了协议和命名的定义,因为 Istio 系统会基于这些定义进行操作约束。在客户端,我们明确定义了传输协议为 gRPC。通过与 CloudWeGo 社区的紧密互动和迭代,我们逐步解决了遇到的各种具体问题。
这里是我们最终确定的整体架构图。我们可以从顶部开始,逐步追踪用户的流量请求是如何被处理的。首先,流量会通过一个名为 ALB Ingress 的负载均衡器进入,这是我们的入口点。随后,流量将进入 Istio 管理的服务网格系统。在服务网格的第一层,我们可以将其抽象为一个 Web Service 层,它类似于我们之前提到的一些框架,比如 Express 框架。接下来是我们的 Kitex RPC 服务层,这里主要部署了核心的内部服务,以及一些可复用的服务。这些服务被集中在 Kitex 的 RPC 层。
继续深入,我们可能会涉及到一些定时任务,这在微服务系统中是非常常见的。例如,CronJob 系统允许我们以 Kitex 客户端的形式去调用 Kitex 服务端,以处理周期性任务。
我们的系统从旧的 Python 单体应用迁移而来,在这个过程中,我们将其抽象为一个简单的 Web Server 依赖。在进行系统迁移时,我们必须解决新旧系统之间的调用问题。为此,我们设定了一个规则:只允许微服务单向调用旧的 Python Web Server。这样做的原因有两个:首先,简化了系统间的交互,减少了复杂性,从而避免了潜在的问题;其次,由于大多数情况下两套系统需要共存,我们需要确保线上流量能够在两个系统中顺畅流通。
对于新服务的开发,我们优先采用新的技术栈,比如使用 Go 语言的框架。这意味着从逻辑层面上,我们的 Go 服务器实际上是依赖于 Python 的。当然,也存在 Python 需要调用 Go 服务器的情况。这种情况下,我们可以将其视为一个迁移的机会,将 Python 服务迁移到 Go,从而促进整个系统向新技术栈的迭代。
二、可复用性与自动化流程
基于 CloudWeGo,Construct 公司建立了一套参考实现和公共库,包括日志记录、监控指标、ID 生成等通用功能,以及特定业务逻辑的封装,极大提升了代码的复用性。Construct 公司实施了自动化的 CI/CD 流水线,通过代码合并后的流水线自动触发,实现了快速迭代和部署。特别是引入的 nilaway code check 流水线,通过静态代码分析,显著降低了系统的崩溃率。接下来,我将具体阐述我们实现这一架构的方法和步骤。
在微服务架构中,可复用性是一个至关重要的要素。我们都明白,转向微服务的初衷是为了更灵活地扩展和维护系统。但在拆分服务的过程中,我们不可避免地面临代码复用的问题。如果我们每次都要从零开始复制或实现服务,这不仅效率低下,而且会增加公司的维护成本。为了解决这一问题,我们提供了一套示例工程,这极大地降低了团队上手和创建新微服务的门槛,同时确保了公司代码的一致性和可维护性,无形中预防了许多潜在问题。
我还想推荐 CloudWeGo 的脚手架工具 cwgo。这个工具与我们之前提到的示例工程有相似之处。我们之所以没有在早期选择使用它,是因为在我们做出技术选型决定时,cwgo 还没有开源。但现在,社区功能已经非常完善。如果现在有人要从零开始构建微服务系统,完全可以利用这套脚手架工具来快速实现,而不必从基础做起。
第三部分与可复用性相关,即 Common lib 的建立。在公司层面或业务层面,我们考虑的是多个业务线之间是否有共通的部分。例如,日志记录、监控指标等公共功能,以及特定业务需求如 ID 生成等。每个公司的需求都不尽相同,因此我们的经验是,可以根据公司的具体情况,将公共库分为通用库和各业务线的基线库,这样可以显著提升代码的复用率和迭代速度。
除了公司和业务层面的公共库,Kitex 官方也提供了许多专业的扩展实现,这些库可以在不同的场景下复用。正如之前提到的 kitex-contrib,我们可以在构建微服务时选用这些官方提供的组件,这不仅可以提升代码的可复用性,还能避免许多常见问题。官方的实现通常更为专业和可靠,其原理与我们自己建立的公共库相似,但适用于不同的应用场景。
接下来,我将介绍我们的持续集成和持续部署(CI/CD)流水线。采用微服务架构意味着我们需要频繁发布众多服务。为了应对这一挑战,我们选择了自动化流程,以简化发布过程。我们的流水线设计遵循标准化步骤,包括镜像构建、小规模部署、全面部署等阶段。这些步骤可以根据团队的特定需求进行选择和调整。自动化流水线的设置,以及代码合并后的自动触发机制,极大地提高了研发团队的开发效率。
此外,我们流水线中还集成了一个关键的质量保证环节——代码检查。在代码合并前,我们设置了名为“nilaway code check”的检查点。这个环节的目的是在代码合并前,通过自动化的代码分析工具,识别潜在的空指针错误。如果发现问题,流水线将阻止代码合并,确保只有高质量的代码进入主分支。我们发现,线上问题中有高达 80%到 90%是由空指针引起的。通过在流水线中加入这一检查点,我们显著提高了系统的稳定性。实际上,我们通过这种方式将系统的 panic 率降低了 90%,从而极大地提升了系统的稳定性和可靠性。
总之,通过精心设计的 CI/CD 流水线和代码检查机制,我们不仅提高了开发效率,还确保了代码质量和系统稳定性,为团队带来了显著的收益。
三、流量管理与泳道系统
我们刚才讨论了 Istio 作为服务网格代理,它负责接管并管理所有流量的分发。现在,我将重点介绍我们如何在 Istio 环境中成功集成 Kitex。
首先,服务发现是集成过程中的一个关键点。Kitex 本身支持多种服务发现机制,我们最终选择的落地方案是让 Kitex 依赖于 Istio 进行服务发现,进而依赖 Kubernetes 来实现。这种设计允许我们实现服务间的有效解耦。当然,根据不同团队的具体情况,也可以选择 Istio 支持的其他服务发现机制进行集成。
其次,我们来谈谈熔断(circuit breaker)机制。这是一个在实际生产环境中常见的需求,特别是当系统中某个依赖出现问题时,我们需要能够迅速将其隔离,以避免影响整个系统的稳定性。在基于 Istio 和 Service Mesh 的架构中,我们通过 Envoy 来管理这一流程。值得注意的是,Kitex 本身也提供了熔断和限流的能力,虽然在我们的调研中,Kitex 的熔断功能没有默认开启,但用户可以根据自己的需求灵活启用这一功能。
通过这样的设计,我们不仅确保了系统的高可用性,也提供了灵活的流量管理策略,以应对不同的业务场景和需求。
Construct 公司利用 Istio 的流量染色功能,开发人员可以在不启动整个微服务集群的情况下,对特定流量进行测试和调试,实现了开发效率和线上用户体验的双重保障。
泳道系统的概念在微服务架构中与传统单体系统有所不同。在微服务场景中,微服务的数量很多,而开发人员只需要修改与他相关的几个微服务,而无需在本地搭建。例如,在大型企业如字节跳动,开发同学是不可能在本地完整搭建整个微服务系统的,硬件资源也不允许。因此,我们需要对流量进行精细管理,实现流量染色,这是 Istio 的强项,它可以高效地管理流量。
在这一过程中,我们使用 HTTP Header 来对流量进行标记和染色。在流量调度时,只有带有特定染色的流量才会被路由到相应染色的服务中,从而确保线上用户不受影响。在 Kitex 中,实现这一点相对直接,我们通过设置 HTTP Header 的键值对来定义泳道信息。
例如,我们可能会设置一个名为“x-asm-prefer-tag”的 header,以此来区分不同的泳道。关键在于如何将这些上下文信息在服务间传递。在 Kitex 中,这可以通过一行简单的代码实现,尽管我们的实现方式较为直接和底层。
此外,Kitex 官方也提供了其他一些实现方式如 OpenTelemetry 的 Baggage,是一套统一的标准且屏蔽了底层细节,可以作为参考。本质上,我们利用了内部的元数据库来传递追踪信息和 HTTP Header 信息,确保它们能够在服务调用链中正确传递。在 Istio 的 VirtualService 层面,我们可以定义路由规则,根据 HTTP Header 的特定值将流量路由到相应的服务。
以一个具体的例子来说,如果检测到value为“sl-test-1”的 HTTP Header,流量就会被路由到特定的服务。如果该服务不存在,流量将回退到线上服务。通过这种方式,我们可以实现泳道系统,使流量从线上服务跳转到灰度环境服务,实现类似这样的灵活跳转。
简而言之,通过 Istio 和 Kitex 的结合使用,我们能够实现复杂的流量管理策略,为开发人员提供了强大的工具来处理微服务架构中的挑战。
四、Mesh 可观测性
Construct 公司结合 Jaeger 和 OpenTelemetry,实现了对服务请求的全面监控。这不仅帮助团队深入理解服务间的调用关系,还为性能优化提供了数据支持。我们将讨论一些关键工具,如分布式追踪系统 Jaeger、性能监控工具 Prometheus 以及可视化界面 Grafana,以及它们与 Kitex 的集成方法。
- 什么是追踪(Tracing)?
追踪,简单来说,就像是一根串联起整个服务调用链的线。如果将服务比作项链上的珍珠,追踪就是那根线,让我们能够沿着链路清晰地看到每个节点——即每个服务的具体信息。这些信息包括耗时、执行顺序和具体的方法调用等。在某种意义上,Tracing为我们对具体请求进行了一次 CT Scan,使我们能够清楚地了解每一层服务在做什么。这样,当出现问题时,我们可以迅速定位原因,比如服务为何响应缓慢。
- Jaeger 与 OpenTelemetry
我们的追踪系统基于 Jaeger 和 OpenTelemetry 构建。Jaeger 是一个由 Uber Technologies 开源的分布式追踪平台,而 OpenTelemetry 则是一套 API、SDK 和工具的集合,用于收集和分析分布式系统的数据。它们帮助我们监控和排查分布式工作流,识别性能瓶颈,追踪根本原因,并分析服务依赖。
- Kitex 与 OpenTelemetry 的集成
在 Kitex 中集成 Jaeger 和 OpenTelemetry 时,我们并没有进行大量定制化处理。因为 Kitex 与开源社区的标准非常契合,我们主要基于官方的追踪提供者进行配置,使其与 Kitex Server 无缝集成。一个关键点是追踪信息的传递。追踪本质上是自上而下串联起来的。我们在下游的收集服务器上定制了一个 middleware,其核心逻辑是将追踪的上下文信息传递下去,确保整个追踪信息能够连贯。
这种定制化的实现在当时是必要的,因为社区尚未有现成的解决方案。但现在,社区已经有了非常成熟的 OpenTelemetry 集成,为那些希望从零开始构建系统的用户提供了便利。当然,用户也可以选择完全自定义实现,这完全取决于具体需求和场景。
继追踪系统之后,我们引入了 Pyroscope,这是一种针对 Pod 级别的性能剖析工具。与追踪系统Tracing关注单个请求不同,Pyroscope 专注于 Pod 的性能问题。在服务端开发中,我们可能经常遇到系统上线后突然崩溃的情况。这时,Pyroscope 就发挥了作用。通过与 Kitex 集成,Pyroscope 可以帮助我们进行性能剖析,定位到具体的问题所在,并进行相应的修复。Pyroscope 的使用非常简单直观,通过与 OpenTelemetry 集成,我们可以轻松地将其应用于 Kitex,实现即插即用的功能。
接下来是 Prometheus 和 Grafana,这两种工具在监控领域广为人知。Istio 对 Prometheus 提供了良好的官方支持,我们在项目中也采用了这一集成方案。通过 Prometheus,我们可以在全局维度关注服务的关键指标,如 QPS 和时延等,并根据不同的 NAMESPACE 和服务进行细粒度的监控。Prometheus 的全局监控策略意味着,当我们引入新的微服务时,它们可以自动继承现有的监控配置,从而显著降低了维护成本。
此外,我们还建立了一个全局的监控报警系统 Alart Manager。它基于 Prometheus 的数据进行分析,并在特定条件下触发报警。例如,如果某个接口在 3 分钟内的平均时延超过 3 秒,Alart Manager 就会发出报警。这种灵活的报警机制可以根据具体的业务场景进行调整。
Kitex 本身也对 Prometheus 提供了支持,使得我们可以在 Istio 集成 Prometheus 的基础上,进一步对 Kitex 服务进行监控和度量,从而更好地了解服务的性能状况。
五、为什么选择 CloudWeGo
最后,我想谈谈我们为什么从零开始构建系统时选择了 CloudWeGo。我们的选择基于几个关键因素:
CloudWeGo 的高性能和高可用性是 Construct 选择它的重要原因。
Kitex 在高并发场景下的性能优势,以及 Istio 的稳定性,为公司业务的持续发展提供了坚实的基础。实际上,在了解到 CloudWeGo 后,我们进行了一些基准测试,结果证实了它的性能确实卓越,这促使我们继续探索。
CloudWeGo 展现出的高可用性给我们留下了深刻印象,至少达到五个九(99.999%)的可用度,在面向消费者的(ToC)场景中表现出色。实际上,它的稳定性甚至超过了我们的预期。稳定性是选择开源组件或系统时的一个重要考虑因素。我们发现 CloudWeGo 的稳定性如此之高,以至于我们几乎感觉不到它的存在,就像我们日常生活中的水电煤以及空气一样,被它包围但又几乎感觉不到。
我们非常看重官方的支持。社区承诺在工作时间内 1-2 小时内响应,而我们的亲身体验甚至比这个时间还要短。在我们最初将 Kitex 与 Istio 集成时,如果没有官方的支持,我们将难以实现。因此,我们深刻感受到官方支持的重要性,它对于解决后续问题至关重要。
强有力的官方支持和活跃的社区生态,为 Construct 公司在技术选型和问题解决上提供了坚实后盾。CloudWeGo 社区的繁荣,不仅意味着丰富的资源和支持,也反映了该项目的成熟度和可靠性。CloudWeGo 背后的社区非常繁荣。全球有超过 16 万的用户关注和 2 万以上的 GitHub 星标,这意味着当你遇到问题时,很可能别人已经遇到过并解决了,这大大加快了问题解决的速度。一个繁荣的社区也反映了开源项目的强大实力。
结语
Construct 公司的案例不仅展示了如何利用 CloudWeGo 框架和 Istio 服务网格构建一个高效、稳定且易于管理的微服务系统,更为同行业其他企业提供了宝贵的转型经验和参考。随着技术的不断进步和社区的持续发展,我们期待看到更多企业在微服务架构转型的道路上取得成功。