我们曾在之前的文章(COBOL程序数据转JSON格式消息·III)提到过Kafka在CICS中的运用,今天我们将通过一个案例,更加全面地介绍Kafka和CICS的集成情况。
如何把CICS应用程序中的消息发送到各大流处理服务器,帮助CICS用户扩大业务范围,一直是CICS努力的方向之一。如今,Kafka已被全球众多企业广泛采用,成为最受欢迎的分布式流处理平台之一。
毫无疑问,CICS也支持和Kafka的友好集成。现在,CICS用户可以轻松地把消息发送到Kafka服务器。在这篇文章中,我们就将介绍运行在CICS Liberty中的Liberty Kafka客户端如何通过调用Kafka客户端API将消息发送到Kafka服务器。
1. Kafka和IBM Event Streams
Apache Kafka是一个分布式的流处理平台,用于发布和订阅流式记录。它起源于LinkedIn,并于2011年成为Apache的开源项目。Kafka快速、可扩展、持久且具有容错能力,多年来发展迅猛。如今,《财富》500强中已有超过三分之一的企业将其用于生产系统。
将Kafka与CICS集成的想法源于多个客户的需求,他们希望实现CICS与Kafka的连接。他们的业务运行在CICS上,但他们逐渐意识到需要将其数据发送到Kafka服务器。
我们的解决方案采用了IBM Event Streams。IBM Event Streams 基于 Apache Kafka构建,是一款高吞吐量且具有容错功能的事件管理平台,可帮助您构建极具响应性的智能化事件驱动型应用。
当前,有三种机制可用于将数据从主机传输到IBM Event Streams:
A.运行在CICS中的Liberty Kafka客户端
我们的项目就是这种情况,它需要起一个运行在CICS中的Liberty JVM服务器。我们开发了一个Liberty Kafka客户端,它实际上是运行在CICS Liberty中的Java应用程序。该客户端通过调用Kafka客户端API将消息发送到Kafka服务器。
B. MQ连接器
如果您拥有IBM MQ许可证,则可以使用MQ事件流连接器连接到IBM Event Streams。IBM创建了一对连接器。MQ源连接器用于将数据从IBM MQ复制到事件流或Apache Kafka,而MQ接收器用于将数据从事件流或Apache Kafka复制到IBM MQ。连接器本身可以运行在USS和zLinux下,也可以在分布式系统上运行。
C. Inbound RESTful APIs
RESTful API允许包括z/OS Connect在内的多个系统将数据发送到IBM Event Streams。这意味着我们可以使用RESTful API将事件从任何主机系统发送到IBM Event Streams。RESTful API是单向的,即从Z到IBM Event Streams。
2. 项目背景
一些供应商正在银行业中寻求新的渠道应用解决方案。我们的一家泰国客户与一家业务供应商合作,开发基于Kafka服务器的银行渠道应用程序。他们的解决方案依赖于主机数据,那要如何将消息从CICS传输到Kafka服务器呢?大家一致认为最好的解决方案是将消息从CICS直接发送到Kafka服务器。
3. 项目架构
我们的解决方案包括以下内容:
- 运行在CICS region中的COBOL程序负责发送应用程序数据。COBOL程序通过EXEC CICS LINK调用运行在CICS Liberty JVM服务器中的Java程序。数据通过CICS channel/container传输。
- 注解Java方法并配置Liberty JVM服务器以从CICS程序调用Java EE应用程序。
Liberty中的Java程序实现以下逻辑:
- 调用JCICS API从CICS channel/container获取数据。
- 调用DFHJSON程序将应用程序数据转换为JSON。
- 通过调用Kafka客户端API,使Kafka生产者客户端将JSON数据发送到Kafka服务器。
4. Link to Liberty (L2L)
使用L2L,我们可以将Liberty JVM服务器中的Java EE应用程序作为CICS交易的初始程序或通过LINK、START或START CHANNEL命令来调用它。L2L允许COBOL、PL/I甚至汇编程序应用程序以标准方式轻松使用Java EE功能。
我们使用L2L功能将Java应用程序暴露成CICS程序,需要执行以下步骤:
1. 配置Liberty JVM服务器
要配置Liberty JVM服务器以支持到Java EE应用程序的链接,要将cicsts:link-1.0功能添加到server.xml中。确保在部署Java应用程序之前添加此功能。
2. 准备Java EE应用程序
对于能被CICS程序调用Java EE应用程序,它必须是纯Java对象(POJO),并带有注解才能启用所需的Java方法。
1 public class LinkToLiberty {
2 @CICSProgram("PUTQ")
3 public void putQ() 4 {
5 try
6 {
7 //JCICS example, Get data from Channal/Container.
8 Channel currentChannel =Task.getTask().getCurrentChannel();
9 if (currentChannel != null)
10 {
11 Container conHead = currentChannel.getContainer("HEAD");
12 if (conHead != null)
13 {
14 byte[] dataArry =conHead.get();
15 Container conPayload = currentChannel.getContainer("PAYLOAD");
16 if(conPayload !=null)
17 {
18 byte[]payloadData = conPayload.get();
19 }
20 // TODO: we will use the data in container here
21 }
22 }
23 }
24 }
25 }
左右滑动查看代码
在上面的代码中,我们用@CICSProgram对putQ()方法进行了注解,并指定其参数以匹配要创建的PROGRAM资源的名称,例如@CICSProgram("PUTQ")。当我们enable Java项目的bundle资源后,CICS会自动创建一个 PROGRAM 资源,PROGRAM的名称就是注解中的参数。
Channel/container是Java程序和COBOL程序之间实现数据通信的最主要方式。本例中我们可以看到程序在最开始获取到当前的channel。然后,我们从channel中通过getContainer 来获取COBOL程序传入的数据。
5. DFHJSON转化应用程序数据为JSON
DFHJSON是CICS提供的一个程序,应用程序可以通过Link DFHJSON实现应用程序数据和JSON数据之间的转换。
在开始之前,JSONTRANSFRM bundle资源必须已经安装并已启用。数据转换是通过使用 JSONTRANSFRM 对象驱动的,该对象根据 JSON 模式绑定文件 .jsbind 创建,该文件包含应用程序数据与JSON 数据之间的映射。因为我们是从JSON模式开始,我们可以运行DFHJS2LS批处理作业以创建应用程序记录的数据映射。
具体步骤如下:
1. 应用程序必须创建一个channel(例如MyJSONChan),并将以下container放入channel中。
- DFHJSON-DATA
- DFHJSON-TRANSFRM
2. 使用LINK PROGRAM API命令链接到CICS提供的可链接接口DFHJSON,将COBOL应用程序数据转换成JSON数据。
EXEC CICS LINK PROGRAM('DFHJSON') CHANNEL('MyJSONChan')
左右滑动查看代码
3. 获取容器 DFHJSON-ERROR 并检查在转换期间是否发生任何错误。
4. 获取容器 DFHJSON-JSON。
代码片段如下:
1 //(1)Create a channel MyJSONChan.
2 jsonChan = Task.getTask().createChannel("MyJSONChan");
3 // Put the contents of the language structure into the DFHJSON-DATA container.
4 Container conJsonData = jsonChan.createContainer("DFHJSON-DATA");
5 conJsonData.put(dataArry);
6 // Put the name of the BUNDLE resource into the DFHJSON-TRANSFRM container.
7 Container conTransfrm = jsonChan.createContainer("DFHJSON-TRANSFRM");
8 conTransfrm.putString("OUTP");
9
10 //(2)Issues an EXEC CICS LINK request to the program DFHJSON to do the transformation.
11
12 Program p = new Program();
13 p.setName("DFHJSON");
14 p.link(jsonChan);
15
16 //(3)Check the DFHJSON-ERROR container to see if there is any error.
17 Container bitconJsonErr = jsonChan.getContainer("DFHJSON-ERROR");
18 if (bitconJsonErr != null)
19 {
20 byte[] ba = bitconJsonErr.get();
21 ByteBuffer bb = ByteBuffer.wrap(ba);
22 int cicsrc = bb.getInt();
23 logger.error("error happens for CICS Json parser! DFHJSON-ERROR="+cicsrc);
24
25 //Check the DFHJSON-ERROR container to get more errors messages.
26 Container conERRORMSG = jsonChan.getContainer("DFHJSON-ERRORMSG");
27 if (conERRORMSG != null)
28 {
29 String ERRORMSG = conERRORMSG.getString();
30 logger.error("DFHJSON-ERRORMSG="+ERRORMSG);
31 }
32 }
33 else
34 {
35 //(4)Get the JSON message from the DFHJSON-JSON container.
36 Container conJson = jsonChan.getContainer("DFHJSON-JSON");
37 if (conJson != null)
38 {
39 sJson = conJson.getString();
40 }
41 }
左右滑动查看代码
6. 性能测试
我们通过使用池对象来提高性能。如果不使用线程池,每当将数据发送到Kafka时都会创建多个对象。线程池可以复用创建成本太高的对象,从而大大提高了性能。
在我们的例子中,我们调用KafkaProducer API将JSON消息发送到Kafka主题 "ACCOUNT-BALANCES-INBOUND-TOPIC"。消息是异步发送的,使用CallBack类并实现onCompletion()方法:
1Properties properties = new Properties();
2
3properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_BROKER_LIST); properties.put(ProducerConfig.CLIENT_ID_CONFIG, CLIENT_ID);
4properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
5
6producer = new KafkaProducer<>(properties);
7
8ProducerRecord data = new ProducerRecord(TOPIC, sJson); 910producer.send(data, new SimpleCallBack(startTime, 0, sJson));
左右滑动查看代码
创建新生产者(KafkaProducer)的成本比较高,每次将数据发送到Kafka时,过程都是新建KafkaProducer对象 > 打开 > 发送 > 关闭。当打开时,Kafka客户端会连接到Kafka服务器以读取元数据,组装消息并再次发送。这是一个成本比较高的过程,为了避免此类问题,我们配置了Kafka对象池。
对象池是一种设计模式,允许重用昂贵的对象。我们使用Apache Commons Pool(org.apache.commons.pool2)创建我们自己的对象池。
我们创建的是一个存储KafkaProducer对象的池。Liberty应用程序在发送数据时通过调用pool.borrowObject借用KafkaProducer,然后在完成发送数据后通过调用pool.returnObject将KafkaProducer返回到池中。
1pool = new GenericObjectPool>(new ProducerFactory(), config); 23producer = ProducerListener.pool.borrowObject(); 4producer.send(data, new SimpleCallBack(startTime, 0, sJson));5ProducerListener.pool.returnObject(producer);
左右滑动查看代码
我们将该项目部署到了客户的系统上,获得了极好的性能——一分钟内发送了3,000条记录,平均响应时间仅为0.0045秒!
如果您有类似需求,需要将消息从CICS发送到Kafka服务器,也可以尝试在系统中实现我们的解决方案哦。
今天是我们在年前的最后一次推送,祝大家新年快乐,我们年后见!
作者:米爱莲
作者:董亚鹏
作者:王典