Cellery:向Kubernetes部署应用程序的代码优先方法

本文要点

  • 尽管微服务架构(MSA)有很多好处,但管理数百个松耦合的微服务很快就会变得很麻烦。这就是设计基於单元格的架构(CBA)的原因。
  • CBA是一种微服务架构模式,它的主要要求是将多个微服务(及其他组件)分组成称为单元格的构建块,以方便管理和重用。
  • 从零开始在容器编排平台上创建CBA很费力。在撰写本文时,Kubernetes是业界广泛采用的容器编排平台;然而,使用YAML编写用于此目的的Kubernetes工件并不是一项简单的任务。
  • Cellery遵循代码优先的方法,处理实现CBA的底层复杂性。
  • Cellery包含一个SDK、一个运行时和一个管理框架。

Cellery简介

Cellery到底是什么,它如何帮助我们在Kubernetes上部署和管理应用程序?Cellery是一种在Kubernetes上构建、集成、运行和管理复合应用程序的代码优先方法。这种复合应用程序的构建块称为单元格——其名称为Cellery,而不是Celery。为了帮你理解单元格和Cellery,让我们看看如何使用Cellery部署、管理和观察一个已有的由谷歌编写的Kubernetes应用程序。但是,在此之前,让我们先了解下单元格是什么以及单元格的工作原理。

单元格是什么?

让我们看一下,为什么需要在微服务架构中使用复合组件。

微服务是构建复杂且不断演化的应用程序的热门选项,它可以缩短上市时间,加快创新速度。每个服务都可以由专门负责该服务的团队独立开发,并且他们可以自由选择任何他们认为合理的技术。最重要的是,微服务是可重用的,每个服务都可以独立伸缩,使团队可以使用最能满足服务资源需求的最佳部署基础设施。开发人员可以对其服务进行本地更改,并在测试完成后立即部署这些更改。那么,这有什么挑战吗?

微服务(包括无服务器函数)的使用正在快速增长,因为组织的目标是提高开发速度和可伸缩性,而且它们还必须调整为面向业务能力的团队。在拥有数十个或数百个应用程序的企业中,管理如此多的松耦合微服务,不仅会成为运营的噩梦,也在团队沟通以及服务发现、版本控制和可观察性等方面提出了挑战。更多的服务、更多的沟通路径、更复杂的网络安排以及更多的潜在故障区。这就有了对高级结构的需求,将多个微服务和无服务器函数聚合到易于管理和重用的构建块中。

基於单元格的架构是一种微服务架构模式,它将系统的微服务、数据和其他功能组件(包括前端应用程序、遗留服务、代理、网关和遗留系统适配器)分组为内聚的、可单独部署的架构单元(称为单元格)。

通常,组件分组的依据是作用范围、所有权和组件之间的相互依赖关系。每个单元格都应该单独设计和开发,并且应该可以独立部署、管理和观察。此外,单元格内的组件可以使用支持的传输协议在单元格内部实现相互通信。然而,所有传入的服务请求必须首先通过单元格网关。该网关使用标准网络协议通过受控的网络端点提供安全API、事件或流。团队可以通过自组织的方式生成可以持续部署和增量更新的单元格。下面是对单元格架构中单元格的一个简单描述:

图1:自包含的架构单元:单元格

实现单元格架构的方法

Cellery设计用于基於单元格架构原则创建Kubernetes应用程序。使用Cellery,我们可以编写代码来定义单元格及其组件,方法是指向现有的容器镜像(其中包含构成单元格的微服务及其他组件),并定义这些组件之间的关系、对其他单元格的依赖和单元格API(网关)。然后就可以使用单元格定义代码生成单元格镜像。实际上,一旦我们将单元格定义代码提交到版本控制存储库中,就可以触发CI/CD管道。像Jenkins这样的CI/CD系统可以构建单元格镜像,对其进行测试,并将其推入容器存储库。然后,CI/CD系统可以拉取单元格镜像并将其部署到相应的生产环境。而且,我们还可以像在Kubernetes上部署的其他任何应用程序一样更新、扩展和观察部署好的单元格。

图2:面向Cellery的DevOps流

使用Cellery创建单元格

简言之,Cellery包含了SDK、运行时和管理框架。安装Cellery时,可以通过Cellery CLI运行命令,执行各种任务。

首先,你应该在本地机器上创建一个Kubernetes集群,或者使用已有的Kubernetes集群作为Cellery运行时环境。输入一个简单的命令command cellery,就会看到一个提示,让你通过交互式CLI选择部署首选项,Cellery 将根据你的首选项为你配置Kubernetes集群。

设置好Cellery运行时环境之后,就可以开始用Cellery语言编写单元格。Cellery语言基于Ballerina编程语言,因此可以使用VSCode和IntelliJIdea作为IDE。要自动生成包含标准导入语句和所需函数的单元格定义文件,可以使用cellery init命令。接下来,你可以定义组件,完成构建,并使用Cellery语言运行逻辑。然后,Cellery编译器将编译代码并使用一个简单的命令cellery build创建相应的Kubernetes构件。

要在Cellery运行时环境上部署单元格,运行cellery run命令并提供必要的参数。你可以使用cellery push命令将构建好的镜像推入单元格镜像存储库,使用cellery pull命令从存储库中拉取单元格镜像,并将构建和部署流集成到CI/CD管道。此外,你还可以通过cellery view命令查看单元格的可视化表示。Cellery还提供了单元格测试功能和可观察性工具,让你可以监控、记录和跟踪单元格。

为什么是Cellery?为什么不使用YAML为单元格配置Kubernetes部署?

对于已经采用容器的组织,开发人员不仅要创建微服务,还要理解容器编制系统如Kubernetes的细微差别。此外,除了准备Kubernetes集群、创建、部署和管理应用程序,从头开始创建CBA涉及到配置服务网格、处理服务身份验证和配置符合CBA原则的安全策略等任务。因此,使用标准Kubernetes资源配置单元格需要一些Kubernetes专业知识。

此外,Kubernetes资源(如pod、服务和部署)是通过YAML文件声明性地创建的。因此,随着部署规模的增加,面对不断增长的、越来越复杂的YAML代码,如果没有成熟的IDE帮助他们提高生产效率,DevOps团队就会陷入挣扎。而且,由于缺乏对函数、抽象和封装等编程概念的支持,YAML本身就鼓励大量重复代码。因此,在Kubernetes上创建复杂的部署意味着DevOps团队必须经历一个冗长而令人畏惧的过程,编写并维护可能长达数千行的YAML文件。这很容易出错。

Cellery使用类型安全的、经过验证的代码而不是YAML来定义部署,而且,它还负责处理配置部署、单元格连接、服务、自动缩放等底层复杂性。此外,在默认情况下,使用Cellery编写的单元格通过单点登录、令牌、基于策略的访问控制和mTLS等安全机制来确保安全。Cellery是围绕DevOps实践而设计的,因此,可以使用蓝/绿部署和金丝雀部署无缝地进行构建、推送、拉取、测试、部署和更新。用户还可以通过监控和跟踪功能观察部署。

简言之,Cellery的目标是简化Kubernetes上应用程序的配置、构建、测试和部署。如上所述,该项目试图从不同的角度解决这个问题,包括开发、DevOps、安全性和可观察性。

Cellery实例

让我们看一个真实的微服务示例,你可以亲自尝试一下。(要了解如何使用Cellery编写单元格代码,可以查看Cellery语法并尝试一些示例。)

为此,我们使用了谷歌的“Hipster Shop”演示应用程序。这里有原始Hipster Shop演示程序的详细信息、源代码、Docker文件等。这个示例适用于Cellery 0.3.0版本。

Hipster Shop应用程序是一个多层次、多语言的微服务应用程序,它是基于Web的电子商务应用程序。用户可以浏览商品,将它们添加到购物车中,使用应用程序购买它们。Hipster Shop由前端和多个微服务组成,通过gRPC相互通信。服务架构如图3所示,表1是Hipster Shop微服务的说明。

图3:Hipster Shop应用的服务架构
服务 语言 说明
frontend Go 暴露一个HTTP服务器来为网站提供服务。不需要注册/登录,会自动为所有用户生成会话ID。
cartservice C# 把用户购物车中的物品保存在Redis中并检索它。
productcatalogservice Go 从一个JSON文件提供产品列表以及搜索产品和获取单个产品的功能。
currencyservice Node.js 将一种货币转换成另一种货币。使用从欧洲央行获取的真实值。这是QPS最高的服务。
paymentservice Node.js 从给定的信用卡(mock)扣除给定的金额,并返回一个交易ID。
shippingservice Go 根据购物车给出运输成本估计。将物品发送到给定的地址(mock)。
emailservice Python 向用户发送一封订单确认邮件(mock)。
checkoutservice Go 检索用户购物车,准备订单,并安排付款、发货和电子邮件通知。
recommendationservice Python 根据购物车中的物品推荐其他产品。
adservice Java 基于给定上下文单词提供文本广告。
loadgenerator Python/Locust 不断向前端发送模拟真实用户购物流的请求。
表1:Hipster Shop应用程序的现有服务

为了将Hipster Shop的微服务映射到基於单元格的架构,我们将这些微服务分组为五个单元格:ads、products、cart、checkout和front-end。我们根据每个微服务单独执行的任务以及它与单元格中其他微服务的关系密切程度设计了这种分类。一个单元格由一个团队拥有,但是一个团队可以拥有一个或多个单元格。定义单元格边界可以基于其他标准,例如组件到组件的连接数量,而不仅限于功能和所有权。要了解更多关於单元格粒度的信息,请点击这里

还有一点非常重要,为了使用Cellery,我们没有对Hipster Shop原来的微服务做任何更改;Cellery仅引用微服务的现有容器镜像。表2列出了单元格及各自的组件,图4进行了说明。

单元格 组件
ads adservice
products productcatalogservice、recommendationservice
cart cartservice、cacheservice
checkout checkoutservice、emailservice、paymentservice、shippingservice、currencyservice
front-end frontendservice
表2:Hipster Shop服务到单元格的映射

图4:Hipster Shop应用程序基於单元格的架构

单元格front-end包含前端应用程序,这是其唯一组件,HTTP流量通过其网关访问单元格,而front-end通过gRPC与其他单元格交互。

单元格checkout是该架构中除front-end之外惟一与外部单元格(products和cart)通信的单元格,剩下的单元格products、ads和cart是独立的单元格,在这些单元格中,只有其内部组件之间发生通信。可以点击这里查看所有完整的单元格定义文件(扩展名为.bal的文件)以及运行和部署Hipster Shop单元格的说明。请注意,这个示例已经在Cellery 0.3.0版本上进行了测试。

创建单元格

让我们看下ads的代码,它包含一个组件:adservice。

ads.bal

import ballerina/config;
import celleryio/cellery;

public function build(cellery:ImageName iName) returns error? {
   int adsContainerPort = 9555;
   // Ad服务组件
   // 基于给定上下文单词提供文本广告。
   cellery:Component adsServiceComponent = {
       name: "ads",
       source: {
           image: "gcr.io/google-samples/microservices-demo/adservice:v0.1.1"
       },
       ingresses: {
           grpcIngress: <cellery:GRPCIngress>{
           backendPort: adsContainerPort,
           gatewayPort: 31406
       }
       },
       envVars: {
           PORT: {
               value: adsContainerPort
           }
       }
   };

   // 单元格初始化
   cellery:CellImage adsCell = {
       components: {
           adsServiceComponent: adsServiceComponent
       }
   };
   return cellery:createImage(adsCell, untaint iName);
}
public function run(cellery:ImageName iName, map<cellery:ImageName> instances) returns error? {
   cellery:CellImage adsCell = check cellery:constructCellImage(untaint iName);
   return cellery:createInstance(adsCell, iName, instances);
}

首先,单元格定义以标准import 语句开始,幷包含两个函数:build和run(如果你使用cellery init命令,那么这些函数将自动生成)。当用户分别执行cellery build和cellery run命令时,build函数和run函数将被调用。

在build 函数中,通过指向公共Docker镜像URL(source)并定义网络访问入口点(ingresses)和环境变量(envVars),定义一个名为adServiceComponent的组件来表示adservice。然后,单元格被初始化并使用名称adsCell定义,之前定义的adServiceComponent被添加到它的组件列表中。然后,使用cellery:createImage方法创建单元格镜像。

最后,run函数将获取构建的单元格镜像(cellery:ImageName iName),其中包含单元格镜像和相应的实例名,并使用cellery:createInstance方法从单元格镜像创建一个正在运行的实例。

单元格内组件间的通信

现在,我们已经看了基本单元格文件的代码结构,让我们看一下有两个或多个组件的单元格的代码,以及如何配置这些组件实现彼此通信。

单元格products有两个组件:productcatalogservice和recommendationservice。如图4所示,recommendationservice需要与productcatalogservice交互,因为它根据购物车中的物品来推荐产品。单元格中组件之间的通信是通过环境变量实现的。

如下面的代码片段所示,recommendationServiceComponent需要通过环境变量(envVars)PRODUCT_CATALOG_SERVICE_ADDR获得productCatalogServiceComponent的地址。此外,productCatalogServiceComponent被标记为dependencies字段下的一个依赖项,这可以确保productCatalogServiceComponent启动并运行,并且可以用于解析依赖项。

products.bal

..
 // 推荐服务组件
 // 根据购物车中的物品推荐其他产品
 cellery:Component recommendationServiceComponent = {
     name: "recommendations",
     source: {
        image: "gcr.io/google-samples/microservices-demo/recommendationservice:v0.1.1"
     },
     ingresses: {
        grpcIngress: <cellery:GRPCIngress>{
        backendPort: recommendationsContainerPort,
        gatewayPort: 31407
       }
     },
     envVars: {
        PORT: {
            value: recommendationsContainerPort
        },
        PRODUCT_CATALOG_SERVICE_ADDR: {
            value: cellery:getHost(productCatalogServiceComponent) + ":" + productCatalogContainerPort
        },
        ENABLE_PROFILER: {
            value: 0
        }
     },
     dependencies: {
        components: [productCatalogServiceComponent]
     }
   };
 ..

单元格以名称productsCell定义并初始化,productCatalogServiceComponent和preferationservicecomponentare均被添加到其组件列表中,如下面的代码片段所示。

..
   // 单元格初始化
   cellery:CellImage productsCell = {
       components: {
           productCatalogServiceComponent: productCatalogServiceComponent,
           recommendationServiceComponent: recommendationServiceComponent
       }
   };
..

单元格间通信

讨论完组件间通信,下面介绍一个单元格中的组件如何与另一个单元格中的组件通信。由于CBA要求所有外部传入的通信必须通过单元格网关进行,因此单元格front-end的代码的唯一组件是Web前端应用程序,并且必须与位于不同单元格中的各种组件通信。

front-end.bal

..
   cellery:Component frontEndComponent = {
       name: "front-end",
       source: {
           image: "gcr.io/google-samples/microservices-demo/frontend:v0.1.1"
       },
       ingresses: {
           portal: <cellery:WebIngress> { // Web ingress is exposed globally.
           port: frontEndPort,
           gatewayConfig: {
               vhost: "my-hipstershop.com",
               context: "/"
               }
           }
       },
..

上面的代码显示了frontEndComponent如何暴露一个HTTP服务器为Hipster Shop网站提供服务。为了与相关的内外部微服务通信,同一个组件需要几个环境变量的值。让我们看一下让frontendcomponent可以与单元格products的组件进行交互的代码。

envVars: {
    ..
    PRODUCT_CATALOG_SERVICE_ADDR: {
      value: ""
    },
    RECOMMENDATION_SERVICE_ADDR: {
      value: ""
    },
    ..
},

如上面的代码片段所示,frontEndServiceComponent需要通过环境变量(envVars)PRODUCT_CATALOG_SERVICE_ADDR和RECOMMENDATION_SERVICE_ADDR分别获得productCatalogServiceComponent和recommendationServiceComponent的地址。

dependencies: {
    cells: {
productsCellDep: <cellery:ImageName>{ org: "wso2cellery", name: "products-cell", ver: "latest"},
..
 }
      }

单元格front-end依赖於单元格products,这种依赖关系是通过上面介绍的frontEndComponent 中的dependencies字段定义的。

cellery:Reference productReference = cellery:getReference(frontEndComponent, "productsCellDep");

frontEndComponent.envVars.PRODUCT_CATALOG_SERVICE_ADDR.value = <string>productReference.gateway_host + ":" +<string>productReference.products_grpc_port;

frontEndComponent.envVars.RECOMMENDATION_SERVICE_ADDR.value = <string>productReference.gateway_host + ":" +<string>productReference.recommendations_grpc_port;

方法cellery: getReference (frontEndComponent productsCellDep)会提供一个指向已部署的products单元格实例的引用,借助这个引用,我们可以解析出上述代码中环境变量PRODUCT_CATALOG_SERVICE_ADDR和RECOMMENDATION_SERVICE_ADDR的值。类似地,front-end单元格通过上述方法与其他单元格通信。

剩下的两个单元格定义文件遵循相同的原则,可以从GitHub库中获取。

cart.bal
单元格cart是一个包含两个组件的独立单元格。

checkout.bal
单元格checkout包含五个组件,为了从checkoutservice调用cartservice和productcatalogservice,它需要分别与单元格cart和products通信。

在完成了所有单元格定义的编码后,接下来可以构建和部署这些单元格了。你还可以将部署Hipster Shop微服务所需的完整Kubernetes YAML文件与Hipstershop Cellery代码进行比较,后者不仅在Kubernetes上部署微服务,而且还围绕这些微服务创建了基於单元格的架构。

构建和部署单元格

请按照这里的说明构建和运行所有的Hipster Shop单元格。

运行独立单元格

现在看一下如何构建和运行ads单元格,它是一个独立的单元格。

打开终端,定位到ads.bal文件所在的位置,运行以下命令构建ads单元格:

$ cellery build ads.bal wso2cellery/ads-cell:latest

我们在Docker Hub中的组织名称是wso2cellery,使用ads-cell作为单元格镜像的名称,使用latest作为标签。执行build命令后可以看到如下输出:

✔ Building image wso2cellery/ads-cell:latest
✔ Removing old Image
✔ Saving new Image to the Local Repository


✔ Successfully built cell image: wso2cellery/ads-cell:latest

What's next?
--------------------------------------------------------
Execute the following command to run the image:
  $ cellery run wso2cellery/ads-cell:latest
--------------------------------------------------------

要以实例名ads-cell运行单元格镜像wso2cellery/ads-cell:latest,运行以下命令:

$ cellery run wso2cellery/ads-cell:latest -n ads-cell

可以看到以下输出:

✔ Extracting Cell Image wso2cellery/ads-cell:latest

Main Instance: ads-cell

✔ Reading Cell Image wso2cellery/ads-cell:latest
✔ Validating dependencies

Instances to be Used:


  INSTANCE NAME           CELL IMAGE            USED INSTANCE   SHARED  
 --------------- ----------------------------- --------------- --------
  ads-cell        wso2cellery/ads-cell:latest   To be Created    -      

Dependency Tree to be Used:

 No Dependencies

? Do you wish to continue with starting above Cell instances (Y/n)? y

✔ Starting main instance ads-cell


✔ Successfully deployed cell image: wso2cellery/ads-cell:latest

What's next?
--------------------------------------------------------
Execute the following command to list running cells:
  $ cellery list instances
--------------------------------------------------------   

运行依赖单元格

现在,以单元格front-end为例,看看如何构建和运行依赖于其他单元格的单元格。build命令与执行单元格ads的命令类似。

$ cellery build front-end.bal wso2cellery/front-end-cell:latest

然而,在运行有依赖项的单元格镜像时,还必须列出单元格所依赖的其他单元格的运行实例的名称。这从单元格front-end的run命令可以看出来,如下所示。

$ cellery run wso2cellery/front-end-cell:latest -n front-end-cell -l cartCellDep:cart-cell -l productsCellDep:products-cell -l adsCellDep:ads-cell -l checkoutCellDep:checkout-cell -d

输出如下:

✔ Extracting Cell Image wso2cellery/front-end-cell:latest

Main Instance: front-end-cell

✔ Reading Cell Image wso2cellery/front-end-cell:latest
⚠ Using a shared instance cart-cell for duplicated alias cartCellDep
⚠ Using a shared instance products-cell for duplicated alias productsCellDep
✔ Validating dependency links
✔ Generating dependency tree
✔ Validating dependency tree

Instances to be Used:

  INSTANCE NAME               CELL IMAGE                  USED INSTANCE       SHARED  
 ---------------- ----------------------------------- ---------------------- --------
  checkout-cell    wso2cellery/checkout-cell:latest    Available in Runtime    -      
  products-cell    wso2cellery/products-cell:latest    Available in Runtime   Shared  
  ads-cell         wso2cellery/ads-cell:latest         Available in Runtime    -      
  cart-cell        wso2cellery/cart-cell:latest        Available in Runtime   Shared  
  front-end-cell   wso2cellery/front-end-cell:latest   To be Created           -      

Dependency Tree to be Used:

 front-end-cell
   ├── checkoutCellDep: checkout-cell
   ├── productsCellDep: products-cell
   ├── adsCellDep: ads-cell
   └── cartCellDep: cart-cell

? Do you wish to continue with starting above Cell instances (Y/n)? y

✔ Starting dependencies
✔ Starting main instance front-end-cell


✔ Successfully deployed cell image: wso2cellery/front-end-cell:latest

What's next?
--------------------------------------------------------
Execute the following command to list running cells:
  $ cellery list instances
--------------------------------------------------------

还可以使用view命令查看单个单元格的图形化表示及其依赖关系。例如,要查看单元格front-end,请键入以下命令:

cellery view wso2cellery/front-end-cell:latest

这会打开一个描述单元格front-end的Web页面,如图5所示。

图5:单元格front-end的图形化表示

可观察性

为了监控和排除已部署单元格的故障,Cellery提供了可观察性工具,包括仪表板。Cellery仪表板显示了许多单元格视图,其中显示了依赖关系图、单元格的运行时指标、对通过网关的请求的端到端分布式跟踪以及单元格组件。所有指标都是从组件和网关收集的,其中包括与Kubernetes pod和节点(包括CPU、内存、网络和文件系统使用情况)相关的系统指标和请求/响应指标(应用程序指标)。


图6:Cellery 可观察性仪表板

真得需要Cellery吗?

如果你正在寻找这个问题的答案,那么你还需要问问自己,微服务项目是否将是云原生的,以及该项目是否会随着时间的推移而增长和进化。如果答案是肯定的,那么你必须记住,管理数百个松耦合的微服务可能很快就会成为一场噩梦。这就是为什么要设计基於单元格的架构,但是在容器编排平台(如Kubernetes)上使用YAML文件从零开始创建CBA绝非易事。这就是Cellery的作用所在,它使开发人员能够遵循代码优先的方法,并处理实现CBA的潜在复杂性,从而真正利用云原生微服务的优势,避免其中的陷阱。

Cellery网站GitHub库中提供了更多有趣的内容和学习材料。

关于作者

Dakshitha Ratnayake是WSO2的企业架构师,他在软件开发、解决方案架构和中间件技术方面有超过10年的经验。这是作者为InfoQ撰写的第一篇文章。

原文链接:

Cellery: A Code-First Approach to Deploy Applications on Kubernetes

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章