关键要点
- 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文件中。
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__# Ballerina tutorial config file with Twitter secrets
clientId = ""
clientSecret = ""
accessToken = ""
accessTokenSecret = ""__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
教程目录
我们将通过迭代改进来完成我们的集成流程。
- 第1部分:创建服务。我们将接受发送自客户端的一个字符串,并将其作为响应的一部分返回。
- 第2部分:通过调用Twitter连接器发送推文。我们将安装一个Twitter连接器,用于将客户端的字符串发布到你的Twitter信息流上。
- 第3部分:使用断路器。我们将使用GET请求向不可靠的外部服务发送字符串,然后添加断路器来处理超时和错误情况。
- 第4部分:将服务部署到Kubernetes。
第1部分:创建服务
让我们创建一个只包含单个资源的服务,这个资源可以通过POST来访问。消息体里将包含一个字符串,我们会将其返回给调用者。
创建homer.bal文件:
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__// `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);
}
}__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
你可以使用“run”命令来编译和运行此服务。
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ 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__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
在另一个控制台中运行curl来调用该服务。资源通常可以通过/hello/hi访问,与代码中提供的名字相对应。不过,我们在代码中使用了注解,现在可以通过“/”来访问资源。
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ curl -X POST -d "Ballerina" localhost:9090
Hello Ballerina!__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
这里有几个有趣的概念:
- 服务是可以绑定到不同协议的一等结构。服务是一种入口,编译器将其打包到服务器中进行部署。Ballerina支持多种入口,包括main(…)函数。
- Ballerina支持各种协议,如WebSocket、gRPC和JMS。资源方法的签名根据服务绑定协议的不同而有所差异。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帐号。我们必须对代码做一些修改:
- 导入包含连接器的Twitter包,用于进行简单的远程调用。
- 将保存在twitter.toml配置文件中的Twitter秘钥传给连接器。
- 从Twitter的响应消息中提取状态信息,并将其放入我们提供给调用方的响应消息中。
Ballerina连接器是一个代码对象,可用它在端点上执行操作。一些共享模块包含了连接器,可将其导入到代码中使用。你可以在Ballerina Central(central.ballerina.io)上或通过CLI找到可用包。
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ ballerina search twitter
Ballerina Central
=================
|NAME | DESCRIPTION | DATE | VERSION |
|-----------------| --------------------------------| ---------------| --------|
|wso2/twitter | Connects to Twitter from Ball...| 2018-04-27-Fri | 0.9.10 |
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
我们可以通过命令行直接下载这个包,这与从远程注册表拉取镜像到本地计算机的方式类似。Ballerina维护了一个主存储库,缓存了所有下载过的包。
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ ballerina pull wso2/twitter__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
更新homer.bal:
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__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);
}
}__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
然后运行它,这次把包含令牌和秘钥的配置文件传给它:
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ ballerina run --config twitter.toml demo.bal __Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
执行与之前相同的curl命令,得到的响应消息将包含Twitter状态。
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ curl -d "My new tweet" -X POST localhost:9090
{"text":"My new tweet #ballerina","id":978399924428550145,"agent":"ballerina"}__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
你应该可以在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:
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__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);
}
}__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__编译代码并运行它,不过为了演示断路器的作用,需要多次运行它。__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ 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"}
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
这里也有更多有趣的知识点:
- Ballerina提供了联合类型,这是一种可以容纳其他不同类型的类型,非常适合用于表示网络概念,因为API可以在单个响应中发送不同格式的消息,并且使用不同的类型来表示数据。
- Ballerina支持一种通用的var数据类型,可以赋给它任何值。带有关键字match的代码块将根据提供的变量类型来执行子代码块。它就像是一种分支逻辑,联合类型中的每种类型都有相应的代码块。
- 断路器成为Homer Simpson服务连接的一部分。这种应用断路器的方式是Ballerina分布式系统原语编译时抽象的一个应用示例。
第4部分:将服务部署到Kubernetes
在当下,一门不提供现代微服务平台原生支持的云原生编程语言会是怎样的?在Ballerina中,可以在源文件中添加注解,用以触发构建任务,为Docker、Kubernetes或用户定义的环境创建部署包。Ballerina的注解系统是可定制的,你可以编写额外的构建器扩展,这些扩展通过操作源码树或注解进行构件后编译。
再次更新homer.bal:
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__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) {
// 资源相关代码不变
}
}__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
就是这样,然后再次构建它:
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ ballerina build demo.bal
@kubernetes:Service - complete 1/1
@kubernetes:ConfigMap - complete 1/1
@kubernetes:Docker - complete 3/3
@kubernetes:Deployment - complete 1/1__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
它将创建一个名为kubernetes的文件夹,其中包含服务的部署文件以及用于创建Docker镜像的Dockerfile:
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ tree
.
├── demo.bal
├── demo.balx
├── kubernetes
│ ├── demo_config_map.yaml
│ ├── demo_deployment.yaml
│ ├── demo_svc.yaml
│ └── docker
│ └── Dockerfile
└── twitter.toml
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
你可以将它部署到Kubernetes上:
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ kubectl apply -f kubernetes/
configmap "hello-ballerina-conf-config-map" created
deployment "ballerina-demo" created
service "ballerina-demo" created__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
让我们看看它是否在运行,并找出哪个外部端口映射到运行在9090端口上的内部服务。在本例中,端口9090映射到外部端口31977:
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ 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
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
我们可以通过外部端口调用我们的服务。
__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__$ curl -d "Tweet from Kubernetes" -X POST http://localhost:31977
{"text":"Tweet from Kubernetes #ballerina", "id":978399924428550145, "agent":"Ballerina"}__Fri Jul 06 2018 11:06:11 GMT+0800 (CST)____Fri Jul 06 2018 11:06:11 GMT+0800 (CST)__
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
