Ballerina 教程:一门用于集成的编程语言

阅读数:636 2018 年 7 月 4 日

话题:语言 & 开发架构

关键要点

  • Ballerina 是一种新的编程语言和平台,目标是让创建跨分布式端点的弹性服务变得更轻松。
  • Ballerina 使用了分布式系统原语的编译时抽象。这为数据转换提供了类型安全性,编译器可以生成构件,如用于将应用部署到 Docker 和 Kubernetes 的 API 网关。
  • Ballerina 定义了一系列关键词来表示集成概念,包括网络端点、服务、流式 SQL 以及 table、json 和 xml 原始类型。IDE 和其他工具可以基于这些语法元素从任意的 Ballerina 代码生成时序图。

Ballerina 是一种语言和平台的组合设计,敏捷且易于集成,旨在简化集成和微服务编程。三年前,WSO2 的架构师发起了 Ballerina 项目,以解决他们在为 EAI、ESB 和工作流产品(如 Apache Synapse 和 Apache Camel)构建集成流程时遇到的挑战。

使用 Ballerina 编写集成流程,并将它们作为可伸缩微服务部署在 Kubernetes 上,这是怎样的一种体验?

本教程将创建一个服务,把由 REST 托管服务发布的 Homer Simpson 报价信息异步发布到 Twitter 帐户上。我们将使用一个断路器,用于处理不可靠的 Homer Simpson 服务,并对负载执行数据转换。

这是 Ballerina 系列教程的第二部分。第一篇文章Ballerina Microservices Programming Language: Introducing the Latest Release and "Ballerina Central"对这门语言的概念进行了介绍。

入门

请先安装 Ballerina,并将其添加到系统路径中。如果你是通过原生平台安装程序安装 Ballerina,那么在安装好以后,路径就设置好了。

我建议你使用 Visual Studio Code 来编写代码,因为 Ballerina 提供了一个语言服务器扩展,它提供了强大的调试器和智能提示功能。

如果要与你开发的服务发生交互,需要安装 curl。

对于 Kubernetes 部署,你需要安装 Docker 和 Kubernetes。本教程使用 Docker Edge,因为它的 Kubernetes 设置很简单,即使是在 Windows 上也是如此。运行 kubectl get pods 命令,确保之前没有部署任何资源。

Twitter 连接器需要用到 Twitter API 的密钥和令牌。注册一个 Twitter 帐号,并通过设置 Twitter 应用程序来获取这四个值。将这四个值放入 twitter.toml 文件中。

复制代码
# Ballerina tutorial config file with Twitter secrets
clientId = ""
clientSecret = ""
accessToken = ""
accessTokenSecret = ""

教程目录

我们将通过迭代改进来完成我们的集成流程。

  1. 第 1 部分:创建服务。我们将接受发送自客户端的一个字符串,并将其作为响应的一部分返回。
  2. 第 2 部分:通过调用 Twitter 连接器发送推文。我们将安装一个 Twitter 连接器,用于将客户端的字符串发布到你的 Twitter 信息流上。
  3. 第 3 部分:使用断路器。我们将使用 GET 请求向不可靠的外部服务发送字符串,然后添加断路器来处理超时和错误情况。
  4. 第 4 部分:将服务部署到 Kubernetes

第 1 部分:创建服务

让我们创建一个只包含单个资源的服务,这个资源可以通过 POST 来访问。消息体里将包含一个字符串,我们会将其返回给调用者。

创建 homer.bal 文件:

复制代码
// `http`包是默认发行版标准库的一部分。
// 这个包包含了一些对象、注解、函数和连接器,
// 在代码中使用它们时使用`http:`命名空间来引用它们。
import ballerina/http;

// 将这个注解添加到服务上,将基础路径从`/hello`改为`/`。
// 这个注解来自之前导入的包。
@http:ServiceConfig {
  basePath: "/"
}
service<http:Service> hello bind {port:9090} {
// `service`代表了一个实现特定协议的 API,并监听`endpoint`。
// 在这里,服务名为“hello”,并与端口 9090 上的一个匿名端点绑定。

  // 将这个注解添加到资源上来改变它的路径,
  // 并只接受 POST 请求。
  @http:ResourceConfig {
      path: "/",
      methods: ["POST"]
  }
  hi (endpoint caller, http:Request request) {
  // 这是服务的一个`resource`。
  // 每个资源代表了一个可调用的 API 端点。`endpoint`代表网络位置。
  // 对于资源来说,调用客户端也是`endpoint`,并作为参数传入。
  // 这个服务也是一个`endpoint`,并扮演着监听器的角色。

      // 从入站请求中抽取消息。
      // getTextPayload() 返回一个 string 和 error 的联合类型。
      // 操作符`check`的意思是,如果返回的是一个字符串,那么就赋值给左边的变量,
      // 否则的话把错误传到上一层。
      string payload = check request.getTextPayload();
      
      // 这里来自`http`包的一个数据结构,
      // 作为发送给调用者的响应消息。
      http:Response res;
      res.setPayload("Hello "+payload+"!\n");


      // 将响应消息发送给访问资源的客户端。
      // `->`是一种特定的语法,表示这个调用是一个网络调用。
      // `_`表示忽略响应或错误。
      _ = caller->respond(res);
  }
}

你可以使用“run”命令来编译和运行此服务。

复制代码
$ ballerina run homer.bal

ballerina: initiating service(s) in 'homer.bal'
ballerina: started HTTP/WS endpoint 0.0.0.0:9090

$ # 你也可以为.balx 创建一个链接可执行文件,并单独运行它 
$ ballerina build homer.bal
$ ballerina run homer.balx

在另一个控制台中运行 curl 来调用该服务。资源通常可以通过 /hello/hi 访问,与代码中提供的名字相对应。不过,我们在代码中使用了注解,现在可以通过“/”来访问资源。

复制代码
$ curl -X POST -d "Ballerina" localhost:9090
Hello Ballerina!

这里有几个有趣的概念:

  • 服务是可以绑定到不同协议的一等结构。服务是一种入口,编译器将其打包到服务器中进行部署。Ballerina 支持多种入口,包括 main(…) 函数。
  • Ballerina 支持各种协议,如WebSocketgRPCJMS。资源方法的签名根据服务绑定协议的不同而有所差异。Ballerina 示例中提供了很多例子。
  • 端点也是一等结构,它们表示网络端点,这些网络端点可以是我们正在开发的服务、调用服务的客户端或者我们调用的远程服务。端点是一种数据结构,具有特定于端点类型的初始化程序和内嵌函数。
  • 注解是附加在对象上的实体。不同的注解附加到不同的对象,如端点、服务或资源。编译器和构建系统根据注解生成构件并改变服务的行为。第三方和生态系统供应商可以添加他们自己的注解和相应的编译器扩展
  • 包是注解、连接器、数据结构和函数的可复用模块。标准库提供了各种各样的包,开发人员也可以创建自己的包,并将它们推送到 Ballerina 注册表中。Ballerina Central 是一个免费开放的 Ballerina 注册表。
  • Ballerina 包含了一个强类型系统,具有多种原始类型,如 json、xml 和 table 等。联合(union)和元组(tuple)类型是 Ballerina 语言的一部分,它通过提供一种类型结构来简化网络编程。这种结构说明了网络接收到的消息体可以有许多不同的形式,每种形式使用不同的类型来表示。
  • Ballerina 强制使用点号来表示本地调用,以及使用箭头“->”表示网络调用。我们可以基于 Ballerina 的语法推导出时序图,不需要开发人员对代码做出修改或添加注解。集成专家使用时序图来沟通其工作流程的结构,而 Ballerina 开发人员能够在 VS Code 或 Ballerina Composer(Ballerina 提供的编辑器)中获得图形化的时序图。
  • Ballerina 被编译成自己的字节码,并在自己的 VM(包含了一个自定义调度器)中执行。在 JVM 语言中,线程被映射成类。然而,Ballerina 的内部线程模型使用 worker 的概念来表示一个工作单元。worker 与线程之间是一一对应的,Ballerina 调度器将优化线程的执行,以确保永远不会出现任何阻塞行为,除非开发人员特别要求。Ballerina 调度器通过箭头语法“->”为每个调用分配一个 worker,当调用返回时,Ballerina 调度度为其分配另一个 worker 来处理响应。在执行网络调用的过程中,客户端处于阻塞状态,但后端使用了两个独立的 worker,所以整个系统将其视为非阻塞的调用和响应。

第 2 部分:通过调用 Twitter 连接器发送推文

我们从客户端获取请求消息,并将其发送给 Twitter 帐号。我们必须对代码做一些修改:

  1. 导入包含连接器的 Twitter 包,用于进行简单的远程调用。
  2. 将保存在 twitter.toml 配置文件中的 Twitter 秘钥传给连接器。
  3. 从 Twitter 的响应消息中提取状态信息,并将其放入我们提供给调用方的响应消息中。

Ballerina 连接器是一个代码对象,可用它在端点上执行操作。一些共享模块包含了连接器,可将其导入到代码中使用。你可以在 Ballerina Central(central.ballerina.io)上或通过 CLI 找到可用包。

复制代码
$ ballerina search twitter
Ballerina Central
=================
|NAME             | DESCRIPTION                     | DATE           | VERSION |
|-----------------| --------------------------------| ---------------| --------|
|wso2/twitter     | Connects to Twitter from Ball...| 2018-04-27-Fri | 0.9.10  |

我们可以通过命令行直接下载这个包,这与从远程注册表拉取镜像到本地计算机的方式类似。Ballerina 维护了一个主存储库,缓存了所有下载过的包。

复制代码
$ ballerina pull wso2/twitter

更新 homer.bal:

复制代码
import ballerina/http;
import wso2/twitter;
import ballerina/config;

// twitter 包定义了可与 Twitter API 发生交互的端点类型。
// 我们需要使用 apps.twitter.com 的 OAuth 数据来初始化它。
// 我们从 toml 文件中读取这些数据,而不是把它们写在代码里。
endpoint twitter:Client tweeter {
  clientId: config:getAsString("clientId"),
  clientSecret: config:getAsString("clientSecret"),
  accessToken: config:getAsString("accessToken"),
  accessTokenSecret: config:getAsString("accessTokenSecret"),
  clientConfig:{}  
};

@http:ServiceConfig {
  basePath: "/"
}
service<http:Service> hello bind {port:9090} {

  @http:ResourceConfig {
      path: "/",
      methods: ["POST"]
  }
  hi (endpoint caller, http:Request request) {
      http:Response res;
      string payload = check request.getTextPayload();

      // 将请求转换成推文 
      if (!payload.contains("#ballerina")){payload=payload+" #ballerina";}

      twitter:Status st = check tweeter->tweet(payload);

      // 转换,生成 JSON 并传回 
      json myJson = {
          text: payload,
          id: st.id,
          agent: "ballerina"
      };

      // 传回 JSON,而不是普通文本 
      res.setPayload(myJson);

      _ = caller->respond(res);
  }
}

然后运行它,这次把包含令牌和秘钥的配置文件传给它:

复制代码
$ ballerina run --config twitter.toml demo.bal 

执行与之前相同的 curl 命令,得到的响应消息将包含 Twitter 状态。

复制代码
$ curl -d "My new tweet" -X POST localhost:9090
{"text":"My new tweet #ballerina","id":978399924428550145,"agent":"ballerina"}

你应该可以在 Twitter 上看到:

这里有一些新的概念:

  • 除了系统包和标准库,也可以导入和使用第三方连接器包。
  • 关键字 endpoint 用于创建带有连接器的对象。连接类型由 twitter:Client 对象来决定。这个对象的初始化需要多个参数,如用于 Twitter 的令牌和秘钥。tweeter 对象具有全局作用域,可以在资源调用块中使用。
  • Twitter 包提供了一个 tweet 函数,可以在 Twitter 连接上调用,比如在资源块中调用 tweeter->tweet(…)。
  • Ballerina 提供了 json 和 xml 原始类型。我们可以使用强类型的原始类型将来自网络的数据映射成数据结构。而在 ESB 中,这种性质的数据转换通常需要使用 XPath 或其他查询语言。

第 3 部分:使用断路器

接下来,我们通过一个外部服务提供的 HTTP REST API 获取 Homer Simpson 报价信息,并将其放在推文中发送给 Tweeter 账号。这个外部服务不是很可靠,尽管大多数响应都很即时,但偶尔也会变慢,可能是因为流量过大。我们添加了一个断路器,防止我们的服务在出现错误或响应时间过长的情况下一直调用 Homer Simpson API。

更新 homer.bal:

复制代码
import ballerina/http;
import wso2/twitter;
import ballerina/config;

// 这个端点是到外部服务的一个连接。
// 断路器是这个连接的一个配置参数。
// 断路器会在碰到特定错误码或出现 500 毫秒超时时断开。
// 断路器会在 3 秒后恢复到初始状态。
endpoint http:Client homer {
 url: "http://www.simpsonquotes.xyz",
 circuitBreaker: {
     failureThreshold: 0,
     resetTimeMillis: 3000,
     statusCodes: [500, 501, 502]
 },
 timeoutMillis: 500
};

endpoint twitter:Client tweeter {
 clientId: config:getAsString("clientId"),
 clientSecret: config:getAsString("clientSecret"),
 accessToken: config:getAsString("accessToken"),
 accessTokenSecret: config:getAsString("accessTokenSecret"),
 clientConfig: {} 
};

@http:ServiceConfig {
 basePath: "/"
}
service<http:Service> hello bind {port: 9090} {

 @http:ResourceConfig {
     path: "/",
     methods: ["POST"]
 }
 hi (endpoint caller, http:Request request) {
     http:Response res;
     
     // 使用 var 来引用 http:Response 和 error 联合类型。
     // 编译器知道如何使用实际的类型。
     var v = homer->get("/quote");

     // 使用 match 处理异常情况或正常输出 
     match v {
         http:Response hResp => {

             // if proper http response use our old code
             string payload = check hResp.getTextPayload();
             if (!payload.contains("#ballerina")){payload=payload+" #ballerina";}
             twitter:Status st = check tweeter->tweet(payload);
             json myJson = {
                 text: payload,
                 id: st.id,
                 agent: "ballerina"
             };
             res.setPayload(myJson);
         }
         error err => {
             // 如果出现错误或断路器断开了,就会调用这段代码 
             res.setPayload("Circuit is open. Invoking default behavior.");
         }
     }
     _ = caller->respond(res);
 }
}
编译代码并运行它,不过为了演示断路器的作用,需要多次运行它。
$ curl -X POST localhost:9090
{"text":"Marge, don't discourage the boy! Weaseling out of things is important to learn. It's what separates us from the animals! Except the weasel. #ballerina","id":986740441532936192,"agent":"ballerina"}

$ curl -X POST localhost:9090
Circuit is open. Invoking default behavior.

$ curl -X POST localhost:9090
Circuit is open. Invoking default behavior.

$ curl -X POST localhost:9090
{"text":"It’s not easy to juggle a pregnant wife and a troubled child, but somehow I managed to fit in eight hours of TV a day.","id":978405287928348672,"agent":"Ballerina"} 

这里也有更多有趣的知识点:

  • Ballerina 提供了联合类型,这是一种可以容纳其他不同类型的类型,非常适合用于表示网络概念,因为 API 可以在单个响应中发送不同格式的消息,并且使用不同的类型来表示数据。
  • Ballerina 支持一种通用的 var 数据类型,可以赋给它任何值。带有关键字 match 的代码块将根据提供的变量类型来执行子代码块。它就像是一种分支逻辑,联合类型中的每种类型都有相应的代码块。
  • 断路器成为 Homer Simpson 服务连接的一部分。这种应用断路器的方式是 Ballerina 分布式系统原语编译时抽象的一个应用示例。

第 4 部分:将服务部署到 Kubernetes

在当下,一门不提供现代微服务平台原生支持的云原生编程语言会是怎样的?在 Ballerina 中,可以在源文件中添加注解,用以触发构建任务,为 Docker、Kubernetes 或用户定义的环境创建部署包。Ballerina 的注解系统是可定制的,你可以编写额外的构建器扩展,这些扩展通过操作源码树或注解进行构件后编译。

再次更新 homer.bal:

import ballerina/http;
import wso2/twitter;
import ballerina/config;

// 导入 kubernetes 包 
import ballerinax/kubernetes;

endpoint twitter:Client tw {
  clientId: config:getAsString("clientId"),
  clientSecret: config:getAsString("clientSecret"),
  accessToken: config:getAsString("accessToken"),
  accessTokenSecret: config:getAsString("accessTokenSecret"),
  clientConfig:{}  
};

// 我们创建一个单独的端点,而不是使用内联的{port:9090}。
// 我们需要这样做,因为这样才能添加 Kubernetes 注解,
// 告诉编译器生成一个 Kubernetes 服务,并将其暴露出来。
@kubernetes:Service {
  serviceType: "NodePort",
  name: "ballerina-demo" 
}
endpoint http:Listener listener {
  port: 9090
};

// 让编译器生成 Kubernetes 部署文件和 Docker 镜像 
@kubernetes:Deployment {
  image: "demo/ballerina-demo",
  name: "ballerina-demo"
}
// 将配置文件传给镜像 
@kubernetes:ConfigMap {
  ballerinaConf: "twitter.toml"
}
@http:ServiceConfig {
 basePath: "/"
}
service<http:Service> hello bind listener {
  @http:ResourceConfig {
      path: "/",
      methods: ["POST"]
  }
  hi (endpoint caller, http:Request request) {
    // 资源相关代码不变 
  }
}

就是这样,然后再次构建它:

$ ballerina build demo.bal
@kubernetes:Service                      - complete 1/1
@kubernetes:ConfigMap                    - complete 1/1
@kubernetes:Docker                       - complete 3/3
@kubernetes:Deployment                   - complete 1/1

它将创建一个名为 kubernetes 的文件夹,其中包含服务的部署文件以及用于创建 Docker 镜像的 Dockerfile:

$ tree
.
├── demo.bal
├── demo.balx
├── kubernetes
│   ├── demo_config_map.yaml
│   ├── demo_deployment.yaml
│   ├── demo_svc.yaml
│   └── docker
│       └── Dockerfile
└── twitter.toml

你可以将它部署到 Kubernetes 上:

$ kubectl apply -f kubernetes/
configmap "hello-ballerina-conf-config-map" created
deployment "ballerina-demo" created
service "ballerina-demo" created

让我们看看它是否在运行,并找出哪个外部端口映射到运行在 9090 端口上的内部服务。在本例中,端口 9090 映射到外部端口 31977:

$ kubectl get pods
NAME                              READY     STATUS    RESTARTS   AGE
ballerina-demo-74b6fb687c-mbrq2   1/1       Running   0          10s

$ kubectl get svc
NAME             TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)  AGE
ballerina-demo   NodePort    10.98.238.0   <none>        9090:31977/TCP  24s
kubernetes       ClusterIP   10.96.0.1     <none>        443/TCP  2d

我们可以通过外部端口调用我们的服务。

$ curl -d "Tweet from Kubernetes" -X POST  http://localhost:31977
{"text":"Tweet from Kubernetes #ballerina", "id":978399924428550145, "agent":"Ballerina"}

Ballerina 还提供了什么?

Ballerina 语言非常健壮,它的语法可以支持广泛的逻辑和基础概念。

  • 网络类型系统。Ballerina 支持数组、记录、map、表、联合类型、可选类型,nil lifting、元组、Any 类型和类型转换。
  • 并发性。Ballerina 的并发性体现在 worker 上,你的服务和函数可以生成 worker、异步执行函数,并使用条件性的 fork/join 语义。
  • 流式处理。一个支持消息传递的内存对象,一个“forever{}”代码块,可以使用流式 SQL 来处理不断传入的事件流。
  • 项目。多开发者协作,包括依赖管理、包的版本控制、构建编配以及注册表中的共享包管理。
  • 集成框架。简化集成的扩展和构件,包括重定向、查询路径、HTTP 缓存、分块、双向 SSL、multipart 请求和响应、HTTP(S)、WebSocket、基于头部的路由、基于内容的路由、重试、gRPC 和 WebSub。
  • Testerina。单元测试框架,用于执行服务测试,提供了执行顺序保证、打桩和分组功能。

关于作者

Tyler Jewell 是 WSO2 的首席执行官,WSO2 是最大的开源集成提供商,也是 Toba Capital 的合作伙伴。他创办了云 DevOps 公司 Codenvy,该公司于 2017 年被红帽公司收购。作为天使投资人和董事会成员,他为 DevOps 领域的公司带来了 1 亿美元的投资,包括 WSO2、Cloudant(被 IBM 收购)、Sauce Labs、Sourcegraph、ZeroTurnaround(被 Rogewave 收购)、InfoQ 和 AppHarbor(被微软收购)。此前,Tyler 曾在 Oracle、Quest、MySQL 和 BEA 工作,并出版了三本有关 Java 的书。

查看英文原文Ballerina Tutorial: A Programming Language for Integration