混沌工程實(shí)踐 - Spring Boot 微服務(wù)應(yīng)用使用 Chaos Monkey

原文首發(fā)與『程序員精進(jìn)』博客,原文鏈接:混沌工程實(shí)踐 - Spring Boot 微服務(wù)應(yīng)用使用 Chaos Monkey

chaos-monkey-aws-banner

我們中有多少人在生產(chǎn)環(huán)境下遇到了系統(tǒng)崩潰或是故障?當(dāng)然答案是所有人,暫時(shí)未遇到的日后也會遇到。如果我們不能避免故障,看起來可行的方案就是在肯定會故障的狀態(tài)下維護(hù)我們的系統(tǒng)。Netflix 基于這個(gè)概念發(fā)明了用于測試他們 IT 基礎(chǔ)設(shè)施韌性(可恢復(fù)能力)的工具——Chaos Monkey。

今天我們將在 Spring Boot 應(yīng)用中使用 Codecentric Chaos Monkey 庫,并且在一個(gè)由多個(gè)微服務(wù)構(gòu)成的示例項(xiàng)目中實(shí)現(xiàn)混沌工程。Chaos Monkey 庫目前與 Spring Boot 2.0 搭配的最新 release 版本是 1.0.1. 但在本次示例項(xiàng)目中將使用 2.0.0-SNAPSHOT 版本,因?yàn)樾碌倪@個(gè)版本有更多的有趣功能。

1. 應(yīng)用中啟用 Chaos Monkey

在 Spring Boot 應(yīng)用中開啟 Chaos Monkey 支持僅需要兩步,首先在項(xiàng)目依賴中添加在 chaos-monkey-spring-boot。

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>chaos-monkey-spring-boot</artifactId>
    <version>2.0.0-SNAPSHOT</version>
</dependency>

再然后,我們在應(yīng)用啟動時(shí)激活 chaos-monkey 的 profile。

$ java -jar target/order-service-1.0-SNAPSHOT.jar --spring.profiles.active=chaos-monkey

2. 示例項(xiàng)目架構(gòu)

示例項(xiàng)目由三個(gè)微服務(wù)組成,每個(gè)微服務(wù)啟動兩個(gè)實(shí)例,另外再加一個(gè)服務(wù)發(fā)現(xiàn)服務(wù)。微服務(wù)在服務(wù)發(fā)現(xiàn)服務(wù)注冊自己,然后彼此間通過 HTTP API 通信。每個(gè)運(yùn)行中的微服務(wù)實(shí)例都引入了 Chaos Monkey 庫,服務(wù)發(fā)現(xiàn)服務(wù)不引入。下面架構(gòu)圖顯示了這個(gè)示例項(xiàng)目的組成。

chaos-monkey-spring-boot-sample-application-architecture

本示例項(xiàng)目的代碼已經(jīng)放在了 GitHub 上,代碼倉庫名為 sample-spring-chaosmonkey ( https://github.com/piomin/sample-spring-chaosmonkey.git )。在克隆 git 代碼倉庫到本地工作副本后,使用 mvn clean install 命令進(jìn)行示例項(xiàng)目的編譯打包。先運(yùn)行服務(wù)發(fā)現(xiàn)服務(wù),然后給每個(gè)微服務(wù)啟動兩個(gè)實(shí)例,當(dāng)然運(yùn)行時(shí)需要通過 -Dserver.port 參數(shù)來指定服務(wù)端口,以下是運(yùn)行服務(wù)的參考命令:

$ java -jar target/discovery-service-1.0-SNAPSHOT.jar
$ java -jar target/order-service-1.0-SNAPSHOT.jar --spring.profiles.active=chaos-monkey
$ java -jar -Dserver.port=9091 target/order-service-1.0-SNAPSHOT.jar --spring.profiles.active=chaos-monkey
$ java -jar target/product-service-1.0-SNAPSHOT.jar --spring.profiles.active=chaos-monkey
$ java -jar -Dserver.port=9092 target/product-service-1.0-SNAPSHOT.jar --spring.profiles.active=chaos-monkey
$ java -jar target/customer-service-1.0-SNAPSHOT.jar --spring.profiles.active=chaos-monkey
$ java -jar -Dserver.port=9093 target/customer-service-1.0-SNAPSHOT.jar --spring.profiles.active=chaos-monkey

3. Chaos Monkey 庫配置

在 chaos-monkey-spring-boot 庫的 2.0.0-SNAPSHOT 版本中,Chaos Monkey 在引入后是默認(rèn)開啟的,可以通過設(shè)置 chaos.monkey.enabled 屬性來開啟或關(guān)閉。默認(rèn)的襲擊方式是延遲,延遲襲擊方式是在每個(gè)請求處理時(shí)添加隨機(jī)的時(shí)延,其中隨機(jī)時(shí)延取值于 chaos.monkey.assaults.latencyRangeStart 和 chaos.monkey.assaults.latencyRangeEnd 兩個(gè)屬性之間的區(qū)間。襲擊的請求數(shù)量由屬性 chaos.monkey.assaults.level 來設(shè)置,這個(gè)屬性取值范圍為 1-10,數(shù)值為 1 時(shí)意味著每個(gè)請求,數(shù)值為 10 時(shí)意味著每第 10 個(gè)請求。另外我們可以開啟另外兩種襲擊方式:異常和 appKiller。在示例項(xiàng)目微服務(wù)中我們關(guān)于 Chaos Monkey 的配置如下:

chaos:
  monkey:
    assaults:
      level: 8
      latencyRangeStart: 1000
      latencyRangeEnd: 10000
      exceptionsActive: true
      killApplicationActive: true
    watcher:
      repository: true
      restController: true

在每個(gè)微服務(wù)的 application.yml 文件中設(shè)置如上內(nèi)容,理論上,我們會開啟 Chaos Monkey 三種襲擊方式。但實(shí)際上,如果我們開啟了延時(shí)和異常的襲擊方式,appKiller 襲擊方式就不會發(fā)生;并且,當(dāng)我們同時(shí)開啟了延時(shí)和異常的襲擊方式,每個(gè)請求都會被攻擊,將與我們設(shè)置的 chaos.monkey.assaults.level 屬性無關(guān)。另外需要注意的是,我們將 restController watcher 開啟了,默認(rèn)情況下是關(guān)閉的。

4. 啟用 Spring Boot Actuator 訪問端口

在 Codecentric Chaos Monkey 庫的 2.0 版本中有個(gè)新的特性:Spring Boot Actuator 訪問端口,通過 management.endpoint.chaosmonkey.enabled 屬性來設(shè)置是否開啟,在應(yīng)用啟動后,就可以通過 HTTP 訪問端口來訪問了。

management:
  endpoint:
    chaosmonkey:
      enabled: true
  endpoints:
    web:
      exposure:
        include: health,info,chaosmonkey

示例項(xiàng)目 chaos-monkey-spring-boot 提供了幾個(gè)訪問端口進(jìn)行配置查看和修改,通過 GET 請求訪問 /chaosmonkey 可以獲取 Chaos Monkey 庫的所有配置,另外可以通過 POST 請求訪問 /chaosmonkey/disable 來關(guān)閉 Chaos Monkey。完整的訪問端口列表詳見:https://codecentric.github.io/chaos-monkey-spring-boot/2.0.0-SNAPSHOT/#endpoints 。

5. 運(yùn)行項(xiàng)目

示例項(xiàng)目中所有的微服務(wù)都將數(shù)據(jù)存儲到了 MySQL,這里我們將使用 Docker 鏡像來跑 MySQL 數(shù)據(jù)庫,運(yùn)行命令如下:

$ docker run -d --name mysql -e MYSQL_DATABASE=chaos -e MYSQL_USER=chaos -e MYSQL_PASSWORD=chaos123 -e MYSQL_ROOT_PASSWORD=123456 -p 33306:3306 mysql

在示例項(xiàng)目中的應(yīng)用都運(yùn)行后(每個(gè)微服務(wù)運(yùn)行兩個(gè)實(shí)例),我們的運(yùn)行環(huán)境架構(gòu)應(yīng)該跟下圖一致:

sample-application-running

當(dāng)應(yīng)用啟動時(shí)我們可以在日志中看到 Chaos Monkey 的啟動信息,大致如下:

spring-boot-with-chaos-moneky-startup-console-log

可以通過 actuator 的 HTTP 訪問端口來查看每個(gè)運(yùn)行中實(shí)例的 Chaos Monkey 配置。

chaos-monkey-sample-application-actuator-endpoint

6. 測試示例項(xiàng)目

我們這里使用性能測試庫 Gatling 來進(jìn)行測試,將創(chuàng)建 20 個(gè)并發(fā)線程,將通過 API 網(wǎng)關(guān)服務(wù)來調(diào)用 order-service,每個(gè)線程調(diào)用 500 次。

class ApiGatlingSimulationTest extends Simulation {

  val scn = scenario("AddAndFindOrders").repeat(500, "n") {
        exec(
          http("AddOrder-API")
            .post("http://localhost:8090/order-service/orders")
            .header("Content-Type", "application/json")
            .body(StringBody("""{"productId":""" + Random.nextInt(20) + ""","customerId":""" + Random.nextInt(20) + ""","productsCount":1,"price":1000,"status":"NEW"}"""))
            .check(status.is(200),  jsonPath("$.id").saveAs("orderId"))
        ).pause(Duration.apply(5, TimeUnit.MILLISECONDS))
        .
        exec(
          http("GetOrder-API")
            .get("http://localhost:8090/order-service/orders/${orderId}")
            .check(status.is(200))
        )
  }

  setUp(scn.inject(atOnceUsers(20))).maxDuration(FiniteDuration.apply(10, "minutes"))

}

測試的 POST 請求訪問端口是 OrderController 的 add(...) 方法,這個(gè)方法將通過 OpenFeign 客戶端來調(diào)用 customer-service 和 product-service。如果顧客用足夠的資金并且相應(yīng)商品有庫存的情況下,就接受訂單通過 PUT 方法對顧客和商品服務(wù)進(jìn)行修改。下面是相應(yīng)的方法實(shí)現(xiàn):

@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    OrderRepository repository;
    @Autowired
    CustomerClient customerClient;
    @Autowired
    ProductClient productClient;

    @PostMapping
    public Order add(@RequestBody Order order) {
        Product product = productClient.findById(order.getProductId());
        Customer customer = customerClient.findById(order.getCustomerId());
        int totalPrice = order.getProductsCount() * product.getPrice();
        if (customer != null && customer.getAvailableFunds() >= totalPrice && product.getCount() >= order.getProductsCount()) {
            order.setPrice(totalPrice);
            order.setStatus(OrderStatus.ACCEPTED);
            product.setCount(product.getCount() - order.getProductsCount());
            productClient.update(product);
            customer.setAvailableFunds(customer.getAvailableFunds() - totalPrice);
            customerClient.update(customer);
        } else {
            order.setStatus(OrderStatus.REJECTED);
        }
        return repository.save(order);
    }

    @GetMapping("/{id}")
    public Order findById(@PathVariable("id") Integer id) {
        Optional order = repository.findById(id);
        if (order.isPresent()) {
            Order o = order.get();
            Product product = productClient.findById(o.getProductId());
            o.setProductName(product.getName());
            Customer customer = customerClient.findById(o.getCustomerId());
            o.setCustomerName(customer.getName());
            return o;
        } else {
            return null;
        }
    }

    // ...

}

如前面第三步所示,我們將 Chaos Monkey 庫設(shè)置了隨機(jī)延時(shí)為 1000 到 10000 毫秒之間,因此我們要設(shè)置下 Feign 和 Ribbon 客戶端的默認(rèn)超時(shí),這里我們設(shè)定讀超時(shí)為 5000 毫秒,這樣有些請求將會引起客戶端超時(shí)。

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
  hystrix:
    enabled: false

下面是訪問 API 網(wǎng)關(guān)的 Ribbon 客戶端超時(shí)設(shè)置,同時(shí)我們需要更改下 Hystrix 設(shè)置,將 zuul 的斷路器給關(guān)閉。

ribbon:
  ConnectTimeout: 5000
  ReadTimeout: 5000

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 15000
      fallback:
        enabled: false
      circuitBreaker:
        enabled: false

接下來我們將運(yùn)行 Gatling 性能測試,到 performance-test 目錄中運(yùn)行 gradle loadTest 命令,測試結(jié)果跟 Chaos Monkey 襲擊設(shè)置以及 Ribbon、Feign 客戶端的超時(shí)設(shè)置都相關(guān),下圖是我們運(yùn)行后的結(jié)果:

chaos-monkey-sample-application-performance-tests

下面是 Gatling 繪制平均響應(yīng)時(shí)間的圖表,需要注意的是示例項(xiàng)目 order-service 的 add 方法將調(diào)用 product-service 和 customer-service。

image.png

下面是 Gatling 繪制請求結(jié)果的圖表,這里顯示了成功和失敗響應(yīng)的情況。Gatling 生成 HTML 報(bào)告在目錄 performance-test/build/gatling-results 。

chaos-monkey-sample-application-gatling-results

譯注:混沌工程擴(kuò)展閱讀

想了解更多關(guān)于混沌工程相關(guān)內(nèi)容,以下資料會有幫助:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容