前言
Spring Cloud Stream,用精簡的語言概括,他本質上其實就是讓開發人員使用消息中間件變得簡單。
他基於Spring Integration並利用Spring Boot提供了自動配置,提供了極爲方便的消息中間件使用體驗。看到這裏會有人認 爲這個開源項目沒有什麼了不起,基於這個點的開源包有很多,甚至自己已經熟知某種中間件的編碼語法何苦重複造輪子, 我就是這當中的一員。
不識廬山真面目,只緣身在此山中
隨着深入瞭解,我發現Stream僅是Pivotal公司在大數據處理方向佈局的一個子集Spring Cloud Data Flow(一款可自由組合的雲原生微服務,用於收集、轉化、存儲和分析數據)。Spring Cloud並沒有在Netflix OSS止步不前,而是繼續定義和完善Pivotal堆棧,把結構化平臺的優勢帶到全方位開發方案當中去。
企業開發中,業務是重要的一部分,數據也同樣是重要的一部分,用Netflix OSS搞定業務架構,Spring Cloud Data Flow應對數據架構,這事就變得有意思,而使用Stream可以統一業務系統和數據系統的中間件編程模型,作爲技術統一規劃的角度來看,讓我最終決定在生產環境中去嘗試Stream。
截止題主止筆,Stream已經支持Kafka/Rabbit MQ/Redis/Gemfire。
一. 同步與異步
使用消息中間件不難,如何用的恰當卻是門學問。
同步與異步這個基礎性的選擇會不可避免的引導我們使用不同的實現。
如果使用同步通信,發起一個遠程服務調用後,調用方會阻塞自己並等待整個操作的完成。如果使用異步通信,調用方不需要等待操作完成就可以返回,甚至可能不需要關心這個操作是否完成與否。兩種方式都有自己適用的場景,我們不擴展討論,這裏只討論某些相比之下更適用於事件驅動的場景
這兩種不同的通信模式有着各自的協作風格,既 請求/響應
和 基於事件
。
對於前者,通常是編排風格,我們會依賴某個中心大腦來指導並驅動整個流程,缺點是中心控制點承擔了太多的職責,他會成爲網狀結構的中心樞紐及邏輯的起點,這個方法容易導致少量的“上帝”服務,而與其打交道的服務通常會淪爲“貧血”的、基於CRUD的服務。
對於後者,通常是協同風格,客戶端發起的不是一個請求,而是發佈一個事件,然後其他協作者接收到該事件,並知道該怎麼做。我們從來不會告知任何人去做任何事,基於事件的系統天生就是異步的。整個系統都很聰明,業務邏輯並非存在某個核心大腦,而是分佈在不同的協作者中。基於事件的協作方式耦合性很低,這意味着你可以在不改變客戶端代碼的情況下,對該事件添加新的訂閱者來完成新增的功能需求。
二. Stream應用模型
- Middleware:一些消息中間件,本文用例使用kafka
- Binder:粘合劑,將Middleware和Stream應用粘合起來,不同Middleware對應不同的Binder。
- Channel:通道,應用程序通過一個明確的Binder與外界(中間件)通信。
- ApplicationCore:Stream自己實現的消息機制封裝,包括分區、分組、發佈訂閱的語義,與具體中間件無關,這會讓開發人員很容易地以相同的代碼使用不同類型的中間件。
Stream能自動發現並使用類路徑中的binder,你也可以引入多個binders並選擇使用哪一個,甚至可以在運行時根據不同的channels選擇不同的binder實現。
三. 消費者分組
發佈-訂閱模型可以很容易地通過共享topics連接應用程序,但創建一個應用多實例的的水平擴展能力同等重要。當這樣做時,應用程序的不同實例被放置在一個競爭的消費者關係中,其中只有一個實例將處理一個給定的消息,這種分組類似於Kafka consumer groups,靈感也來源於此。每個消費者通過spring.cloud.stream.bindings.<channelName>.group
指定一個組名稱,channelName
是代碼中定義好的通道名稱,下文會有介紹。
消費者組訂閱是持久的,如果你的應用指定了group
,那即便你這個組下的所有應用實例都掛掉了,你的應用也會在重新啓動後從未讀取過的位置繼續讀取。但如果不指定group
Stream將分配給一個匿名的、獨立的只有一個成員的消費組,該組與所有其他組都處於一個發佈-訂閱關係中,還要注意的是匿名訂閱不是持久的,意味着如果你的應用掛掉,那麼在修復重啓之前topics中錯過的數據是不能被重新讀取到的。所以爲了水平擴展和持久訂閱,建議最好指定一個消費者組。
四. 分區
首先,你要放空你之前kafka分區的相關知識,從零開始去領會Stream分區,以免造成理解上的困擾。
Stream提供了一個通用的抽象,用於統一方式進行分區處理,和具體使用的中間件無關,因此分區可以用於自帶分區的代理(如kafka)或者不帶分區的代理(如rabbiemq),這句話要反覆讀幾遍。
Stream支持在一個應用程序的多個實例之間數據分區,N個生產者的數據會發送給M個消費者,並保證共同的特性的數據由相同的消費者實例處理,這會提升你處理能力。
Stream使用多實例進行分區數據處理是一個複雜設置,分區功能需要在生產者與消費者兩端配置,SpringCloudDataFlow可以顯著的簡化過程,而且當你沒有用SpringCloudDataFlow時,會給你的配置帶來一些不便,需要你提前規劃好,而不能再應用啓動後動態追加。
下面是生產者有效的和典型的配置(Output Bindings)
spring.cloud.stream.bindings.<channelName>.producer.partitionKeyExpression=payload.id
spring.cloud.stream.bindings.<channelName>.producer.partitionCount=5
分區key的值是基於partitionKeyExpression計算得出的,用於每個消息被髮送至對應分區的輸出channel,partitionKeyExpression是spirng EL表達式用以提取分區鍵
下面是消費者有效的和典型的配置(Input Bindings)
spring.cloud.stream.bindings.input.consumer.partitioned=true
spring.cloud.stream.instanceIndex=3
spring.cloud.stream.instanceCount=5
instanceCount
表示應用實例的總數,instanceIndex
在多個實例中必須唯一,並介於0~(instanceCount
-1)之間。實例的索引可以幫助每個實例確定唯一的接收數據的分區,正確的設置這兩個值十分重要,用來確保所有的數據被消費,以及應用實例接收相互排斥不重複消費。
五. 編程模型
- 引入pom依賴
- 配置binder參數
- 定義通道
- 配置通道綁定參數
-
通過
@EnableBinding
觸發綁定 -
消費者通過
@StreamListener
監聽 - 配置分區、分組信息
引入pom依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
也可以引入spring-cloud-stream-binder-kafka
,這個少依賴了web和actuater的功能,這兩個功能根據項目實際情況定製更合理,不需要的情況下沒必要依賴。同理你可以引入spring-cloud-stream-binder-redis/rabbit
配置binder參數
SpringBoot項目啓動會掃描到classpath中的kafka binder,並會用默認參數去連接本地的kafka服務和zookeeper服務,如果本地沒有默認配置啓動的這兩個服務,一定會啓動失敗。所以我們要指定配置。
spring.cloud.stream.kafka.binder.brokers=10.79.96.52:9092
spring.cloud.stream.kafka.binder.zk-nodes=10.79.96.52:2182
spring.cloud.stream.kafka.binder.minPartitionCount=1
spring.cloud.stream.kafka.binder.autoCreateTopics=true
spring.cloud.stream.kafka.binder.autoAddPartitions=false
本例中配置的後三項配置值和默認值一致,當然可根據自己的需求定義。
這種配置有些討巧,這個是kafka binder提供的Binder-Specific Configuration,這種方式讓配置更看上去更清爽一些,但如果按照Stream的配置語義,應該如下配置
spring.cloud.stream.bindings.<channelName>.binder=<binderName>
spring.cloud.stream.binders.<binderName>.type=kafka
spring.cloud.stream.binders.<binderName>.environment.spring.cloud.stream.kafka.binder.brokers=10.79.96.52:9092
spring.cloud.stream.binders.<binderName>.environment.spring.cloud.stream.kafka.binder.zk-nodes=10.79.96.52:2182
先爲channel對應的binder設置一個<binderName>
,再根據這個<binderName>
設置binder的type和environment。如果我們的應用只連接一個kafka,那我們完全可以用上面的配置方法,看起來更簡潔。如果我們的應用要連接多個kafka服務,那我們必須用下面的配置方案,通過<binderName>
來完成不同kafka服務的識別與隔離。
定義通道
Stream應用可以有任意數目的input和output通道,可通過@Input和@Output註解在接口中定義。註解默認通道名字爲方法名 ,當然也可以自定義channel名字,@Input("myinputchannel"),下面的例子就完成了通道的定義,Stream在運行時會自動生成這個接口的實現類。
public interface Barista {
@Input
SubscribableChannel orders();
@Output
MessageChannel hotDrinks();
@Output
MessageChannel coldDrinks();
}
Stream爲了方便開發者,內置了三個接口,在簡單業務背景下,我們不用如上所述的去定義通道,直接利用預置通道會更便捷。這三個接口分別是Source
,Sink
,Processor
。
Source
用於有單個輸出(outbound)通道的應用,通道名稱爲output
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
Sink
用於有單個輸入(inbound)通道的應用,通道名稱爲input
public interface Sink {
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}
Processor
用於單個應用同時包含輸入和輸出通道的情況,通道名稱分別爲output
和input
。
public interface Processor extends Source, Sink {
}
配置通道綁定參數
輸入通道的綁定,本例中使用Sink
定義輸入通道,根據上面所述<channelName>
=input
spring.cloud.stream.bindings.input.destination=wsh-topic-01
spring.cloud.stream.bindings.input.group=s3
spring.cloud.stream.bindings.input.consumer.concurrency=1
spring.cloud.stream.bindings.input.consumer.partitioned=false
輸出通道的綁定,本例中使用Source
定義輸出通道,根據上面所述<channelName>
=output
spring.cloud.stream.bindings.output.destination=wsh-topic-01
spring.cloud.stream.bindings.output.content-type=text/plain
spring.cloud.stream.bindings.output.producer.partitionCount=1
#spring.cloud.stream.bindings.output.producer.partitionKeyExpression=payload.id
通過@EnableBinding觸發綁定
現在binder配置好了,channel也配置好了,需要做的就是將binder和channel在代碼中綁定起來。
生產者端
@EnableBinding(Source.class)
public class SendService {
@Autowired
private Source source;
public void sendMessage(String msg) {
try {
source.output().send(MessageBuilder.withPayload(msg).build());
} catch (Exception e) {
e.printStackTrace();
}
}
}
消費者通過@StreamListener監聽
消費者端
@EnableBinding(Sink.class)
public class MsgSink {
@StreamListener(Sink.INPUT)
public void messageSink(Object payload) {
System.out.println("Received: " + payload);
}
}
配置分區、分組信息
具體配置上文有提到,不重複描述,額外提一下spring.cloud.stream.kafka.binder.autoAddPartitions
這個配置默認是false
,通常情況下會產生無法啓動的問題,強烈建議配置成true。
這裏面的原理大致描述如下,比如你啓動了一個生產者並配置producer.partitionCount
=5,那麼Stream底層是需要kafka提供5個kafka分區(注意Stream的5個分區
和 kafka的5個分區此時相等是巧合,請分開理解),如果kafka中如果沒有目標topics,Stream會在啓動的時候在kafka中創建5個分區,併成功啓動,但是如果kafka中已經有了目標topics,並且目標topics不足5個分區,那麼生產者啓動失敗。所以必須設置autoAddPartitions
=true,生產者才能在啓動的時候自動將kafka中的目標topics分區擴展成5個,方能啓動成功。
如果此刻生產者啓動成功,你會啓動消費者,如果消費者你規劃了5個實例,每個實例支持2個併發(concurrency
=2),那麼每個Stream底層需要5*2=10個kafka分區(而此時kafka的目標topics只有5個分區),消費者也會啓動失敗,這種情況下需要將消費者的autoAddPartitions
=true。
autoAddPartitions
=true 有得也有失。得到的上文已經描述,這裏再提一下失去的。 還拿上文舉例,生產者啓動了5個kafka分區,所以生產者實例只會往這5個分區中輸出,這樣就導致消費者擴展出來的另外5個分區收不到數據,所以要重啓生產者,用以重新計算生產者與底層kafka分區的關係。 官方文檔提到使用SpringCloudDataFlow可以顯著的簡化過程,我還沒有嘗試。
六. Content Type
@StreamListener
是Stream提供的註解,Spring
Integration也有一個類似功能的註解@ServiceActivator
,兩者都有監聽通道功能,區別是@StreamListener
可以根據contentType去解析數據,比如一個json格式的數據,@StreamListener
可以自動解析成對象Vote
@EnableBinding(Processor.class)
public class TransformProcessor {
@Autowired
VotingService votingService;
@StreamListener(Processor.INPUT)//讀取input通道的數據
@SendTo(Processor.OUTPUT)//經過方法處理後輸出到output通道
public VoteResult handle(Vote vote) {
return votingService.record(vote);
}
}
七. 項目代碼
SpringCloudStream GitHub
文章出處:Spring Cloud Blog http://blog.spring-cloud.io/blog/sc-stream.html