天天看點

用gradle建構spring-cloud工程用docker運作

這是一個基于gradle建構工具的spring cloud微服務架構超級簡單入門教程。

spring cloud為開發人員提供了快速搭建分布式系統的一整套解決方案,包括配置管理、服務發現、斷路器、路由、微代理、事件總線、全局鎖、決策競選、分布式會話等等。它可以直接在PC上使用Java的main方法運作叢集。

另外說明spring cloud是基于springboot的,是以需要開發中對springboot有一定的了解。

**本教程是教你如何使用spring cloud,以及建構鏡像和本地單機運作叢集,如果你需要學習更進階叢集部署技術以及devops/CI/CD,比如docker swarm, kubernetes以及rancher等等,請為我點贊,我會繼續編寫相關部落格。本部落格所有源碼均在GitHub springcloud-quickstart。

我建議大家在IDE内自行建立工程然後将那些示例代碼片段在你已經了解了的前提下複制到你自己的代碼内再運作起來。**

spring cloud依賴管理

  1. 在/gradle.properties檔案内申明gradle全局公共變量。我們主要用它來定義springCloud版本号,springboot版本号,以及其他一些公共變量
## dependency versions.
springBootVersion=.RELEASE
springCloudVersion=Edgware.RELEASE
### docker configuration
#gradle docker plugin version
transmodeGradleDockerVersion=
#This configuration is for docker container environment to access the local machine host,in Chinese is "主控端" ip.
hostMachineIp=
           
  1. 在/build.gradle檔案内申明springboot gradle插件
buildscript {
    repositories {
        maven { url "https://repo.spring.io/libs-milestone/" }
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}
           
  1. 在/build.gradle檔案内為所有gradle project引入springcloud公共依賴
allprojects {
    apply plugin: 'org.springframework.boot'
    repositories {
        maven { url "https://repo.spring.io/libs-milestone/" }
        jcenter()
    }
    dependencyManagement {
        imports {
            //spring bom helps us to declare dependencies without specifying version numbers.
            mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        }
    }
}
           

注意,上面imports的mavenBom非常重要,它幫我們管理了springCloud各種零散的jar包的版本。有了它,我們在對springCloud元件的依賴引入時,不需要指定具體的元件版本了,這簡直是炒雞友善啊。

  1. /settings.gradle檔案

    它的作用是幫我們在IDE内自動組織項目結構(project structures)的,幫我們避開idea/eclipse内配置工程結構的複雜操作,有興趣可以讀一下源碼。

服務注冊中心/discovery/eureka-server

  1. 本示例使用的是Spring Cloud Netflix Eureka ,eureka是一個服務注冊和發現子產品,公共依賴部分已經在根路徑的build.gradle中給出,

    eureka-server子產品自身依賴在/discovery/eureka-server/build.gradle檔案配置如下:

dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-eureka-server')
}
           
  1. eureka是一個高可用的元件,不依賴後端緩存,每一個執行個體注冊之後需要向注冊中心發送心跳,是在eureka-server的記憶體中完成的,在預設情況下erureka-server也是一個eureka client,必須要指定一個server位址。eureka-server的配置檔案appication.yml:
server:
  port: 
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
           

另請注意一點:很多網上的教程以及spring官方的教程上将’service-url’寫成’serviceUrl’這是錯誤的!

3. eureka-server的springboot入口main application類:

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
           

啟動這個main方法,然後通路 http://localhost:8761

代碼詳見/discovery/eureka-server子產品。

4. eureka-client服務注冊用戶端(service provider)

服務提供方,比如一個微服務,作為eureka client身份可以将自己的資訊注冊到注冊中心eureka-server内。

/discovery/eureka-demo-client/build.gradle檔案指定依賴如下:

dependencies {
    compile "org.springframework.cloud:spring-cloud-starter-eureka"
}
           

springboot入口main類:com.example.EurekaDemoClientApplication.java

@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class EurekaDemoClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaDemoClientApplication.class, args);
    }
    @Value("${server.port}")
    private int port;


    @RequestMapping("/hi")
    public String hi() {
        return "hi, my port=" + port;
    }
}
           

application.yml

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 
spring:
  application:
    name: eureka-demo-client
           

執行main方法啟動springboot後,可以通路http://localhost:8763/hi 檢視springboot restApi效果。

通路http://localhost:8761 (eureka-server控制台)檢視服務注冊效果。

依次類推,再啟動另外一個/discovery/eureka-demo-client0,請再次檢視服務注冊效果。

服務路由和負載均衡/routing

以上/discovery/eureka-demo-client和/discovery/eureka-demo-client0我們可以把它看作是服務提供者service provider,這裡開始定義服務消費者,即對服務提供者進行調用的的用戶端。

當同一個微服務啟動了多個副本節點後,我們對該服務的調用就需要一個負載均衡器來選擇其中一個節點來進行調用,這就是springcloud-ribbon提供的功能。而feign則是對springcloud ribbon的一個封裝,友善使用的。這裡不深入介紹ribbon了,它本質就是一個借助服務注冊發現實作的一個負載均衡器。

下面來分析feign源碼:

/routing/routing-feign/build.gradle

dependencies{
    compile "org.springframework.cloud:spring-cloud-starter-feign"
    compile "org.springframework.cloud:spring-cloud-starter-eureka"
}
           

com.example.RoutingDemoFeignApplication.java

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class RoutingDemoFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(RoutingDemoFeignApplication.class, args);
    }
}
           

com.example.CallHiService.java接口,指明service provider微服務名: eureka-demo-client

@FeignClient(value = "eureka-demo-client")
public interface CallServiceHi {
    @RequestMapping(value = "/hi", method = RequestMethod.GET)
    String sayHiFromClientOne(@RequestParam(value = "name") String name);
}
           

com.example.HiController.java 友善我們驗證負載均衡結果:

@RestController
public class HiController {
    @Autowired
    private CallServiceHi hiServiceCaller;


    @RequestMapping("hi")
    public String hi(@RequestParam String name) {
        return hiServiceCaller.sayHiFromClientOne(name);
    }
}
           

application.yml需要指明服務注冊中心的位址,進而可以擷取到所有目标節點資訊,進而實作負載的功能

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 
spring:
  application:
    name: service-feign
           

運作main方法,啟動springboot,然後請多次通路http://localhost:8765/hi?name=happyyangyuan 檢視負載效果。

預期的輸出結果輪流為:hi happyyangyuan, my port=8763 / hi happyyangyuan, my port=8762

調用鍊追蹤/call-chain

/call-chain内的demo展示的是使用Spring Cloud Sleuth實作的分布式系統的調用鍊追蹤方案,它相容支援了zipkin,隻需要在build.gradle檔案中引入相應的依賴即可。

/call-chain/zipkin-server

顧名思義,就是springCloud Sleuth内置的zipkin-server內建,以來配置/call-chain/zipkin-server/build.gradle:

dependencies {
    compile "io.zipkin.java:zipkin-server"
    compile "io.zipkin.java:zipkin-autoconfigure-ui"
}
           

com.example.ZipkinServerApplication.java,注意加入@EnableZipkinServer注解:

@SpringBootApplication
@EnableZipkinServer
public class ZipkinServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZipkinServerApplication.class, args);
    }
}
           

application.properties指明zipkin端口

執行com.example.ZipkinServerApplication.java的main方法啟動,然後通路http://localhost:9411

/call-chain/zipkin-clients調用鍊模拟

這裡模拟調用鍊: zipkin-client –調用–> zipkin-client0 –調用–> zipkin-client1

/call-chain/zipkin-clients/build.gradle指定三個zipkinClients的公共依賴配置:
subprojects{
    dependencies {
        compile 'org.springframework.cloud:spring-cloud-starter-zipkin'
        /*這兩個依賴已經被傳遞依賴了,是以不需要申明
        compile "org.springframework.cloud:spring-cloud-sleuth-zipkin-stream"
        compile "org.springframework.cloud:spring-cloud-starter-sleuth"*/
        compile "org.springframework.cloud:spring-cloud-starter-feign"
        compile "org.springframework.cloud:spring-cloud-starter-eureka"
    }
}
           

注意到,服務調用我們使用的的是feign用戶端,并且引入了服務發現eureka用戶端和zipkin用戶端。

zipkin用戶端配置

配置檔案依然是/call-chain/zipkin-clients/zipkin-client/src/main/resources/application.properties

server.port=
spring.zipkin.base-url=http://localhost:
spring.application.name=zipkin-client
eureka.client.service-url.defaultZone=http://localhost:/eureka/
#The percentage of call-chaining messages to be sent to sleuth provider (zipkin for example).
#Value range is 0.0~1.0, defaults to 0.1
spring.sleuth.sampler.percentage=
           

上面的配置檔案,注意一下三點

- 給定zipkin-server端位址:spring.zipkin.base-url=http://localhost:9411

- 給定eureka-server位址:eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

- 友善調試給定調用鍊追蹤資料采用比例為100%,對應配置值為1,取值範圍是0~1:spring.sleuth.sampler.percentage=1

zipkin用戶端ZipkinClientApplication代碼邏輯

com.example.ZipkinClientApplication.java使用feign實作了對zipkin-client0的調用:

@SpringBootApplication
@RestController
@EnableDiscoveryClient
@EnableFeignClients
public class ZipkinClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZipkinClientApplication.class, args);
    }
    @Autowired
    private FeignServiceInterface feignService;
    @RequestMapping(value = "/")
    public String home() {
        return feignService.callServiceFeign("no name");
    }
    @FeignClient(value = "zipkin-client0")
    public interface FeignServiceInterface {
        @RequestMapping(value = "/", method = RequestMethod.GET)
        String callServiceFeign(@RequestParam(value = "name") String name);
    }
}
           

不知道你注意到沒有,zipkin用戶端激活功能不需要什麼鬼類似“@EnableXxx”之類的注解,滿足以下兩點就可以激活zipkin用戶端了資料采集了:

- 配置zipkin-server通路位址

- 引入zipkin 用戶端依賴

就是這麼簡單!

其他兩個zipkin-client0、zipkin-client1依次類推,我就不再粘貼代碼了。

驗證調用鍊看效果

啟動zipkin-server、zipkin-client、zipkin-client0、zipkin-client1,啟動方式你們都懂的。

然後通路http://localhost:8988 即可将調用鍊日志資料發送給zipkin-server,然後你再通路http://localhost:9411 檢視調用鍊的展示。界面操作太簡單,我就不貼圖了。

集中配置管理/config

在實際微服務實作的分布式應用中,微服務數量是比較多的,節點數就更多了,我們不可能去每個節點裡面去修改和維護那些配置檔案的。我們需要一個統一的地方去定義和管理這些配置,springCloud Config提供了這樣一個功能,我們隻需要使用VCS版本管理控制系統比如git來維護一份統一配置,然後由springCloudConfig server讀取這些配置,并可以提供給其他所有springCloudConfig Client來讀取。使用VCS不僅可以讓我們可以追溯所有的曆史版本的配置檔案,而且也實作了統一管理。

config server

demo在/config/config-server内

先引入config需要的依賴,build.gradle:

dependencies {
    compile('org.springframework.cloud:spring-cloud-config-server')
}
           

com.example.ConfigServerApplication.java加入@EnableConfigServer注解

@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
           

application.properties:

spring.application.name=config-server
server.port=
##spring.cloud.config.server.git.uri
# Please change the following configuration to point to your own configuration git repo url.
spring.cloud.config.server.git.uri=https://github.com/happyyangyuan/springcloud-configuration.git
# Local git repo url for test only.
# spring.cloud.config.server.git.uri=${HOME}/ideaProjects/spring/configurations-demo-git
# in case of the default /tmp dir deletion.
spring.cloud.config.server.git.basedir=config-repo
#Branch name for the repository.
spring.cloud.config.label=master
           

這裡配置檔案注意幾點:

- 既可以使用遠端git位址也可以使用本地git哦,比如你做測試做實驗時。

- linux系統内config server預設是将git檔案緩存在在本地的/tmp路徑内,但是許多Linux系統會定期清理/tmp檔案的,導緻配置失效。

config client

在實際分布式應用裡面,很多微服務應用都有自己獨立的配置檔案的,由于分布式應用比較分散,管理麻煩,是以我們可以考慮把微服務連接配接到spring cloud config server上,從config server讀取集中配置。

一個簡單的config reader

demo在 /config/config-reader路徑内

依賴引入:

dependencies {
    compile "org.springframework.cloud:spring-cloud-starter-config"
    compile "org.springframework.cloud:spring-cloud-starter-eureka"
}
````
bootstrap.properties




<div class="se-preview-section-delimiter"></div>

```properties
spring.application.name=config-reader
spring.cloud.config.label=master
spring.cloud.config.profile=profileName
spring.cloud.config.uri=http://localhost:/
           

application.yml

server:
  port: 
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
           

注意事項:

- spring.cloud.config.label=master配置是指定config用戶端讀取遠端git哪個分支,如果不配置,就是讀取config-server給定的預設分支了。

- spring.cloud.config.profile=profileName是指定config用戶端讀取遠端git内application-profileName.properties而檔案配置的。

- 如果找不到對應配置就會fall back到“通用層”配置檔案。

- bootstrap先于application配置加載,一些基礎配置要放在bootstrap裡面,基礎配置如demo所示。

com.example.ConfigReaderApplication.java

@SpringBootApplication
@RestController
@EnableDiscoveryClient
public class ConfigReaderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigReaderApplication.class, args);
    }
    @Value("${message}")
    private String value;
    @RequestMapping(value = "/")
    public String read() {
        return value;
    }
}
           

啟動application,然後通路http://localhost:8881 檢視效果吧。

spring cloud bus 消息總線

不知道你有沒有發現,即使你把git内的配置修改了,以上config-reader是沒法自動重新整理配置的,必須重新開機服務才可以。spring cloud bus可以解決這個問題,讓我們的配置可以動态重新整理。

這裡以/config/config-reader-with-bus為例來講解。

引入依賴,build.gradle:

dependencies {
    compile "org.springframework.cloud:spring-cloud-starter-bus-amqp"
    compile "org.springframework.cloud:spring-cloud-starter-config"
    compile "org.springframework.cloud:spring-cloud-starter-eureka"
}
           

這裡引入了一個spring-cloud-starter-bus-amqp,它是spring cloud bus規範的一種實作,基于amqp協定。接入我們比較熟悉的rabbitMQ隊列服務。

bootstrap.yml配置如下:

server:
  port: 
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: config-reader
  cloud:
    config:
      label:  master
      profile:  dev
      uri:  http://localhost:8888/
  rabbitmq:
    host: yourRabbitMqHost
    port:  #yourRabbitPort
    username: rabbitUserName
    password: rabbitUserPwd
management:
  security:
    enabled: false
           

注意事項:

- rabbitmq用戶端配置請自行填充以便你能完成本次demo效果測試。

- management.security.enabled=false配置是為了友善我們後面測試的,後面詳細說明。

com.example.ConfigReaderWithBusApplication類

@SpringBootApplication
@EnableDiscoveryClient
@RestController
@RefreshScope
public class ConfigReaderWithBusApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigReaderWithBusApplication.class, args);
    }
    /**
     * Waning: {@link RefreshScope} does not support private properties
     */
    @Value("${message}")
    String message;
    @RequestMapping("/")
    public String home() {
        return message;
    }
}
           

注意事項:

- @RefreshScope注解是必須的,否則無法實作動态重新整理配置

- @Value(“${message}”)注解的成員必須不能是private私有,否則無法重新整理。這是我測試得到的結論,各位也可以自行驗證一下。

啟動application,然後通路http://localhost:8882 看看配置是否讀取出來。然後修改你的git對應的配置檔案,然後用postman/curl等http用戶端工具調用如下API:http://localhost:8882/bus/refresh 觸發配置更新,如果提示無權限調用此接口,可以配置為禁用management.security.enabled=false,然後再次通路http://localhost:8882 看看配置是否有更新。

-/config/config-reader-with-bus0是另外一個config client的demo,它是用來驗證一次重新整理/bus/refresh則所有支援了消息總線的用戶端都統一自動重新整理配置的功能。親,動手試試吧。

服務網關/api-gateway

本demo /api-gateway/zuul 展示的是使用spring cloud Zuul實作的網關服務。Zuul的主要功能是路由轉發和過濾器,路由功能是微服務的一部分,比如/api/user轉發到到user服務,/api/shop轉發到到shop服務。zuul預設和Ribbon結合實作了負載均衡的功能。

依賴管理build.gradle:

dependencies {
    compile "org.springframework.cloud:spring-cloud-starter-zuul"
    compile "org.springframework.cloud:spring-cloud-starter-eureka"
}
           

注意這裡,ribbon的依賴不需要加入,因為它會被zuul傳遞依賴得到,服務發現用戶端依賴spring-cloud-starter-eureka必須要加入。

application.yml

server:
  port: 
eureka:
  client:
    service-url:
      defaultZone: http://localhost:/eureka
spring:
  application:
    name: zuul
zuul:
  routes:
    api0:
      path: /api0/**
      serviceId: eureka-demo-client
           

根據配置的轉發規則可以看到,對zuul的/api0/** 的請求将全部轉發到服務eureka-demo-client上,我們可以在這裡配置多個路由轉發規則。

com.example.ZuulApplication

@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
}
           

啟動ZuulApplication檢視效果: http://localhost:8769/api0/hi?name=happyyangyuan

斷路器 待補充

待補充

容器化運作方案

  1. 先建立一個容器網絡,以便多個容器(微服務)之間可以互相通信,指令為

    docker network create springcloud-quickstart

  2. 修改根路徑内的gradle.properties檔案中的”hostMachineIp”配置為主控端IP位址,這很重要!
  3. 在根目錄執行gradle dockerBuild指令,建構完畢後,可使用docker images檢視。
  4. 由于幾乎所有的其他微服務元件都依賴服務發現,是以先啟動服務注冊服務端,使用如下指令運作:

    docker run --network springcloud-quickstart -p 8761:8761 com.example/eureka-server:0.0.1-SNAPSHOT

    發現服務(eureka-server)需要端口暴露,以便我們可以在容器外面通路到它的控制台,位址是http://localhost:8761 ,建議端口映射與内部端口一緻。
  5. 啟動其他服務,啟動方式依次類推,除了zipkin-server和zuul網關,其他微服務元件是可以不暴露端口到外部的,列舉幾個關鍵節點啟動指令。
    • zuul網關啟動

      docker run --network springcloud-quickstart -p 8769:8769 com.example/zuul:0.0.1-SNAPSHOT

    • zipkin調用鍊追蹤

      docker run --network springcloud-quickstart -p 9411:9411 com.example/zipkin-server:0.0.1-SNAPSHOT

    • 配置伺服器啟動

      docker run --network springcloud-quickstart -p 8888:8888 com.example/config-server:0.0.1-SNAPSHOT

    • 其他

      docker run --network springcloud-quickstart com.example/<applicationName>:0.0.1-SNAPSHOT

關于容器化建構的附加說明

  1. gradle dockerBuild指令會周遊所有子project,并自動建構出所有微服務的鏡像。我使用的是alpine+jre8,如果本地沒有這個鏡像,會從dockerHub下載下傳alpine-jre基礎鏡像,第一次可能會比較久。alpine+jre整個基礎鏡像是80m左右,主要是jre比較大,再加上springCloud微服務的n多個jar包,最終應用鏡像大小是120m左右。算是目前我能做到的最小的鏡像。小歸小,但是也有缺點:
    • alpine系統内置的不是我們熟悉的bash shell,而是ash shell。
    • 内置的jre,不提供jdk的很多調試指令,愛搞jvm調試的你們懂得。
  2. 我們使用的是se.transmode.gradle:gradle-docker插件,有興趣可以GitHub檢視它的使用說明。

請按順序學習,日後我的GitHub star數增加後我會補充更多細節…