Java中的纖程庫 - Quasar
最近遇到的一個問題大概是微服務架構中經常會遇到的一個問題:
服務 A 是我們開發的系統,它的業務需要調用 B 、 C 、 D 等多個服務,這些服務是通過http的訪問提供的。 問題是 B 、 C 、 D 這些服務都是第三方提供的,不能保證它們的響應時間,快的話十幾毫秒,慢的話甚至1秒多,所以這些服務的Latency比較長。幸運地是這些服務都是集群部署的,容錯率和并發支持都比較高,所以不擔心它們的并發性能,唯一不爽的就是就是它們的Latency太高了。
系統A會從Client接收Request, 每個Request的處理都需要多次調用B、C、D的服務,所以完成一個Request可能需要1到2秒的時間。為了讓A能更好地支持并發數,系統中使用線程池處理這些Request。當然這是一個非常簡化的模型,實際的業務處理比較復雜。
可以預見,因為系統B、C、D的延遲,導致整個業務處理都很慢,即使使用線程池,但是每個線程還是會阻塞在B、C、D的調用上,導致I/O阻塞了這些線程, CPU利用率相對來說不是那么高。
當然在測試的時候使用的是B、C、D的模擬器,沒有預想到它們的響應是那么慢,因此測試數據的結果還不錯,吞吐率還可以,但是在實際環境中問題就暴露出來了。
概述
最開始線程池設置的是200,然后用HttpUrlConnection作為http client發送請求到B、C、D。當然HttpUrlConnection也有一些坑,比如 Persistent Connections 、 Caveats of HttpURLConnection ,跳出坑后性能依然不行。
通過測試,如果B、C、D等服務延遲接近0毫秒,則HttpUrlConnection的吞吐率(線程池的大小為200)能到40000 requests/秒,但是隨著第三方服務的響應時間變慢,它的吞吐率急劇下降,B、C、D的服務的延遲為100毫秒的時候,則HttpUrlConnection的吞吐率降到1800 requests/秒,而B、C、D的服務的延遲為100毫秒的時候HttpUrlConnection的吞吐率降到550 requests/秒。
增加 http.maxConnections 系統屬性并不能顯著增加吞吐率。
如果增加調用HttpUrlConnection的線程池的大小,比如增加到2000,性能會好一些,但是B、C、D的服務的延遲為500毫秒的時候,吞吐率為3800 requests/秒,延遲為1秒的時候,吞吐率為1900 requests/秒。
雖然線程池的增大能帶來性能的提升,但是線程池也不能無限制的增大,因為每個線程都會占用一定的資源,而且隨著線程的增多,線程之間的切換也更加的頻繁,對CPU等資源也是一種浪費。
切換成其它http的實現,比如netty,也無法帶來更好的性能。系統A 嚴重依賴Http協議,而Http協議又是一個 blocking 協議,只有等到Response返回才可以發送下一個請求(雖然有些http server支持http pipelining,但是我們無法保證/控制第三方的B、C、D支持這個特性)。
下面列出了一些常用的http client:
- JDK’s URLConnection uses traditional thread-blocking I/O.
- Apache HTTP Client uses traditional thread-blocking I/O with thread-pools.
- Apache Async HTTP Client uses NIO.
- Jersey is a ReST client/server framework; the client API can use several HTTP client backends including URLConnection and Apache HTTP Client.
- OkHttp uses traditional thread-blocking I/O with thread-pools.
- Retrofit turns your HTTP API into a Java interface and can use several HTTP client backends including Apache HTTP Client.
- Grizzly is network framework with low-level HTTP support; it was using NIO but it switched to AIO .
- Netty is a network framework with HTTP support (low-level), multi-transport, includes NIO and native (the latter uses epoll on Linux).
- Jetty Async HTTP Client uses NIO.
- Async HTTP Client wraps either Netty, Grizzly or JDK’s HTTP support.
- clj-http wraps the Apache HTTP Client.
- http-kit is an async subset of clj-http implemented partially in Java directly on top of NIO.
- http async client wraps the Async HTTP Client for Java.
這個列表摘自 High-Concurrency HTTP Clients on the JVM ,不止于此,這篇文章重點介紹基于java纖程庫quasar的實現的http client庫,并比較了性能。我們待會再說。
回到我前面所說的系統,如何能更好的提供性能?有一種方案是借助其它語言的優勢,比如Go,讓Go來代理完成和B、C、D的請求,系統A通過一個TCP連接與Go程序交流。第三方服務B、C、D的Response結果可以異步地返回給系統A。
Go的優勢在于可以實現request-per-goroutine,整個系統中可以有成千上萬個goroutine。 goroutine是輕量級的,而且在I/O阻塞的時候可以不占用線程,這讓Go可以輕松地處理上萬個鏈接,即使I/O阻塞也沒問題。Go和Java之間的通訊協議可以通過Protobuffer來實現,而且它們之間只保留一個TCP連接即可。
當然這種架構的修改帶來系統穩定性的降低,服務A和服務B、C、D之間的通訊增加了復雜性。同時,因為是異步方式,服務A的業務也要實現異步方式,否則200個線程依然等待Response的話,還是一個阻塞的架構。
通過測試,這種架構可以帶來穩定的吞吐率。 不管服務B、C、D的延遲有多久,A的吞吐率能維持15000 requests/秒。當然Go到B、C、D的并發連接數也有限制,我把最大值調高到20000。
這種曲折的方案的最大的兩個弊病就是架構的復雜性以及對原有系統需要進行大的重構。 高復雜性帶來的是系統的穩定性的降低,包括部署、維護、網絡狀況、系統資源等。同時系統要改成異步模型,因為系統業務線程發送Request后不能等待Go返回Response,它需要從Client接收更多的Request,而收到Response之后它才繼續執行剩下的業務,只有這樣才不會阻塞,進而提到系統的吞吐率。
將系統A改成異步,然后使用HttpUrlConnection線程池行不行?HttpUrlConnection線程池還是導致和B、C、D通訊的吞吐率下降,但是Go這種方案和B、C、D通訊的吞吐率可以維持一個較高的水平。
考慮到Go的優勢,那么能不能在Java中使用類似Go的這種goroutine模型呢?那就是本文要介紹的Java纖程庫: [Quasar]( http://docs.paralleluniverse.co/quasar/)。
quasar初步
Java官方并沒有纖程庫。但是偉大的社區提供了一個優秀的庫,它就是Quasar。
創始人是 Ron Pressler和Dafna Pressler ,由Y Combinator孵化。
Quasar is a library that provides high-performance lightweight threads, Go-like channels, Erlang-like actors, and other asynchronous programming tools for Java and Kotlin.
Quasar提供了高性能輕量級的線程,提供了類似Go的channel,Erlang風格的actor,以及其它的異步編程的工具,可以用在Java和Kotlin編程語言中。Scala目前的支持還不完善,我想如果這個公司能快速的發展壯大,或者被一些大公司收購的話,對Scala的支持才能提上日程。
你需要把下面的包加入到你的依賴中:
- Core (必須) co.paralleluniverse:quasar-core:0.7.5[:jdk8] (對于 JDK 8,需要增加jdk8 classifier)
- Actor co.paralleluniverse:quasar-actors:0.7.5
- Clustering co.paralleluniverse:quasar-galaxy:0.7.5
- Reactive Stream co.paralleluniverse:quasar-reactive-streams:0.7.5
- Kotlin co.paralleluniverse:quasar-kotlin:0.7.5
Quasar fiber依賴java instrumentation修改你的代碼,可以在運行時通過java Agent實現,也可以在編譯時使用ant task實現。
通過java agent很簡單,在程序啟動的時候將下面的指令加入到命令行:
-javaagent:path-to-quasar-jar.jar
對于maven來說,你可以使用插件 maven-dependency-plugin ,它會為你的每個依賴設置一個屬性,以便在其它地方引用,我們主要想使用 ${co.paralleluniverse:quasar-core:jar} :
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.5.1</version>
<executions>
<execution>
<id>getClasspathFilenames</id>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
</plugin>
然后你可以配置 exec-maven-plugin 或者 maven-surefire-plugin 加上agent參數,在執行maven任務的時候久可以使用Quasar了。
官方提供了一個 Quasar Maven archetype ,你可以通過下面的命令生成一個quasar應用原型:
git clone https://github.com/puniverse/quasar-mvn-archetype
cdquasar-mvn-archetype
mvn install
cd..
mvn archetype:generate -DarchetypeGroupId=co.paralleluniverse -DarchetypeArtifactId=quasar-mvn-archetype -DarchetypeVersion=0.7.4-DgroupId=testgrp -DartifactId=testprj
cdtestprj
mvn test
mvn clean compile dependency:properties exec:exec
如果你使用gradle,可以看一下gradle項目模版: Quasar Gradle template project 。
最容易使用Quasar的方案就是使用Java Agent,它可以在運行時instrument程序。如果你想編譯的時候就使用AOT instrumentation(Ahead-of-Time),可以使用Ant任務 co.paralleluniverse.fibers.instrument.InstrumentationTask ,它包含在quasar-core.jar中。
Quasar最主要的貢獻就是提供了輕量級線程的實現,叫做fiber(纖程)。Fiber的功能和使用類似Thread, API接口也類似,所以使用起來沒有違和感,但是它們不是被操作系統管理的,它們是由一個或者多個ForkJoinPool調度。一個idle fiber只占用400K內存,切換的時候占用更少的CPU,你的應用中可以有上百萬的fiber,顯然Thread做不到這一點。這一點和Go的goroutine類似。
Fiber并不意味著它可以在所有的場景中都可以替換Thread。當fiber的代碼經常會被等待其它fiber阻塞的時候,就應該使用fiber。
對于那些需要CPU長時間計算的代碼,很少遇到阻塞的時候,就應該首選thread
以上兩條是選擇fiber還是thread的判斷條件,主要還是看任務是I/O blocking相關還是CPU相關。幸運地是,fiber API使用和thread使用類似,所以代碼略微修改久就可以兼容。
Fiber特別適合替換哪些異步回調的代碼。使用 FiberAsync 異步回調很簡單,而且性能很好,擴展性也更高。
類似Thread, fiber也是用Fiber類表示:
newFiber<V>() {
@Override
protectedVrun()throwsSuspendExecution, InterruptedException {
// your code
}
}.start();
與Thread類似,但也有些不同。Fiber可以有一個返回值,類型為泛型V,也可以為空Void。 run 也可以拋出異常 InterruptedException 。
你可以傳遞 SuspendableRunnable 或 SuspendableCallable 給Fiber的構造函數:
newFiber<Void>(newSuspendableRunnable() {
publicvoidrun()throwsSuspendExecution, InterruptedException {
// your code
}
}).start();
甚至你可以調用Fiber的join方法等待它完成,調用 get 方法得到它的結果。
Fiber繼承Strand類。Strand類代表一個Fiber或者Thread,提供了一些底層的方法。
逃逸的Fiber(Runaway Fiber)是指那些陷入循環而沒有block、或者block fiber本身運行的線程的Fiber。偶爾有逃逸的fiber沒有問題,但是太頻繁會導致性能的下降,因為需要調度器的線程可能都忙于逃逸fiber了。Quasar會監控這些逃逸fiber,你可以通過JMX監控。如果你不想監控,可以設置系統屬性 co.paralleluniverse.fibers.detectRunawayFibers 為 false 。
fiber中的 ThreadLocal 是fiber local的。 InheritableThreadLocal 繼承父fiber的值。
Fiber、SuspendableRunnable 、SuspendableCallable 的 run 方法會拋出SuspendExecution異常。但這并不是真正意義的異常,而是fiber內部工作的機制,通過這個異常暫停因block而需要暫停的fiber。
任何在Fiber中運行的方法,需要聲明這個異常(或者標記@Suspendable),都被稱為suspendable method。
反射調用通常都被認為是suspendable, Java8 lambda 也被認為是suspendable。不應該將類構造函數或類初始化器標記為suspendable。
synchronized 語句塊或者方法會阻塞操作系統線程,所以它們不應該標記為suspendable。Blocking線程調用默認也不被quasar允許。但是這兩種情況都可以被quasar處理,你需要在Quasar javaagent中分別加上 m 和 b 參數,或者ant任務中加上 allowMonitors 和 allowBlocking 屬性。
quasar原理
Quasar最初fork自 Continuations Library 。
如果你了解其它語言的coroutine, 比如Lua,你久比較容易理解quasar的fiber了。 Fiber實質上是 continuation , continuation可以捕獲一個計算的狀態,可以暫停當前的計算,等隔一段時間可以繼續執行。Quasar通過instrument修改suspendable方法。Quasar的調度器使用 ForkJoinPool 調度這些fiber。
Fiber調度器FiberScheduler是一個高效的、work-stealing、多線程的調度器。
默認的調度器是 FiberForkJoinScheduler ,但是你可以使用自己的線程池去調度,請參考 FiberExecutorScheduler 。
當一個類被加載時,Quasar的instrumentation模塊 (使用 Java agent時) 搜索suspendable 方法。每一個suspendable 方法 f 通過下面的方式 instrument:
它搜索對其它suspendable方法的調用。對suspendable方法 g 的調用,一些代碼會在這個調用 g 的前后被插入,它們會保存和恢復fiber棧本地變量的狀態,記錄這個暫停點。在這個“suspendable function chain”的最后,我們會發現對 Fiber.park 的調用。 park 暫停這個fiber,扔出 SuspendExecution異常。
當 g block的時候,SuspendExecution異常會被Fiber捕獲。 當Fiber被喚醒(使用unpark), 方法 f 會被調用, 執行記錄顯示它被block在g的調用上,所以程序會立即跳到 f 調用 g 的那一行,然后調用它。最終我們會到達暫停點,然后繼續執行。當 g 返回時, f 中插入的代碼會恢復f的本地變量。
過程聽起來很復雜,但是它只會帶來3% ~ 5%的性能的損失。
下面看一個簡單的例子, 方法m2聲明拋出SuspendExecution異常,方法m1調用m2和m3,所以也聲明拋出這個異常,最后這個異常會被Fiber所捕獲:
publicclassHelloworld{
staticvoidm1()throwsSuspendExecution, InterruptedException {
String m = "m1";
System.out.println("m1 begin");
m = m2();
m = m3();
System.out.println("m1 end");
System.out.println(m);
}
staticString m2()throwsSuspendExecution, InterruptedException {
return"m2";
}
staticString m3()throwsSuspendExecution, InterruptedException {
return"m3";
}
staticpublicvoidmain(String[] args)throwsExecutionException, InterruptedException {
newFiber<Void>("Caller",newSuspendableRunnable() {
@Override
publicvoidrun()throwsSuspendExecution, InterruptedException {
m1();
}
}).start();
}
}
反編譯這段代碼 (一般的反編譯軟件如jd-gui不能把這段代碼反編譯java文件, Procyon 雖然能反編譯,但是感覺反編譯有錯。所以我們還是看字節碼吧):
@Instrumented(suspendableCallSites={16,17}, methodStart=13, methodEnd=21, methodOptimized=false)
staticvoidm1()
throwsSuspendExecution, InterruptedException
{
// Byte code:
// 0: aconst_null
// 1: astore_3
// 2: invokestatic 88 co/paralleluniverse/fibers/Stack:getStack ()Lco/paralleluniverse/fibers/Stack;
// 5: dup
// 6: astore_1
// 7: ifnull +42 -> 49
// 10: aload_1
// 11: iconst_1
// 12: istore_2
// 13: invokevirtual 92 co/paralleluniverse/fibers/Stack:nextMethodEntry ()I
// 16: tableswitch default:+24->40, 1:+64->80, 2:+95->111
// 40: aload_1
// 41: invokevirtual 96 co/paralleluniverse/fibers/Stack:isFirstInStackOrPushed ()Z
// 44: ifne +5 -> 49
// 47: aconst_null
// 48: astore_1
// 49: iconst_0
// 50: istore_2
// 51: ldc 2
// 53: astore_0
// 54: getstatic 3 java/lang/System:out Ljava/io/PrintStream;
// 57: ldc 4
// 59: invokevirtual 5 java/io/PrintStream:println (Ljava/lang/String;)V
// 62: aload_1
// 63: ifnull +26 -> 89
// 66: aload_1
// 67: iconst_1
// 68: iconst_1
// 69: invokevirtual 100 co/paralleluniverse/fibers/Stack:pushMethod (II)V
// 72: aload_0
// 73: aload_1
// 74: iconst_0
// 75: invokestatic 104 co/paralleluniverse/fibers/Stack:push (Ljava/lang/Object;Lco/paralleluniverse/fibers/Stack;I)V
// 78: iconst_0
// 79: istore_2
// 80: aload_1
// 81: iconst_0
// 82: invokevirtual 108 co/paralleluniverse/fibers/Stack:getObject (I)Ljava/lang/Object;
// 85: checkcast 110 java/lang/String
// 88: astore_0
// 89: invokestatic 6 com/colobu/fiber/Helloworld:m2 ()Ljava/lang/String;
// 92: astore_0
// 93: aload_1
// 94: ifnull +26 -> 120
// 97: aload_1
// 98: iconst_2
// 99: iconst_1
// 100: invokevirtual 100 co/paralleluniverse/fibers/Stack:pushMethod (II)V
// 103: aload_0
// 104: aload_1
// 105: iconst_0
// 106: invokestatic 104 co/paralleluniverse/fibers/Stack:push (Ljava/lang/Object;Lco/paralleluniverse/fibers/Stack;I)V
// 109: iconst_0
// 110: istore_2
// 111: aload_1
// 112: iconst_0
// 113: invokevirtual 108 co/paralleluniverse/fibers/Stack:getObject (I)Ljava/lang/Object;
// 116: checkcast 110 java/lang/String
// 119: astore_0
// 120: invokestatic 7 com/colobu/fiber/Helloworld:m3 ()Ljava/lang/String;
// 123: astore_0
// 124: getstatic 3 java/lang/System:out Ljava/io/PrintStream;
// 127: ldc 8
// 129: invokevirtual 5 java/io/PrintStream:println (Ljava/lang/String;)V
// 132: getstatic 3 java/lang/System:out Ljava/io/PrintStream;
// 135: aload_0
// 136: invokevirtual 5 java/io/PrintStream:println (Ljava/lang/String;)V
// 139: aload_1
// 140: ifnull +7 -> 147
// 143: aload_1
// 144: invokevirtual 113 co/paralleluniverse/fibers/Stack:popMethod ()V
// 147: return
// 148: aload_1
// 149: ifnull +7 -> 156
// 152: aload_1
// 153: invokevirtual 113 co/paralleluniverse/fibers/Stack:popMethod ()V
// 156: athrow
// Line number table:
// Java source line #13 -> byte code offset #51
// Java source line #15 -> byte code offset #54
// Java source line #16 -> byte code offset #62
// Java source line #17 -> byte code offset #93
// Java source line #18 -> byte code offset #124
// Java source line #19 -> byte code offset #132
// Java source line #21 -> byte code offset #139
// Local variable table:
// start length slot name signature
// 53 83 0 m String
// 6 147 1 localStack co.paralleluniverse.fibers.Stack
// 12 99 2 i int
// 1 1 3 localObject Object
// 156 1 4 localSuspendExecution SuspendExecution
// Exception table:
// from to target type
// 49 148 148 finally
// 49 148 156 co/paralleluniverse/fibers/SuspendExecution
// 49 148 156 co/paralleluniverse/fibers/RuntimeSuspendExecution
}
這段反編譯的代碼顯示了方法m被instrument后的樣子,雖然我們不能很清楚的看到代碼執行的樣子,但是也可以大概地看到它實際在方法的最開始加入了此方法的棧信息的檢查(#0 ~ #49,如果是第一次運行這個方法,則直接運行,然后在一些暫停點上加上一些棧壓入的處理,并且可以在下次執行的時候直接跳到上次的暫停點上。
官方的工程師關于Quasar的instrument操作如下:
- Fully analyze the bytecode to find all the calls into suspendable methods. A method that (potentially) calls into other suspendable methods is itself considered suspendable, transitively.
- Inject minimal bytecode in suspendable methods (and only them) that will manage an user-mode stack, in the following places:
- At the beginning we’ll check if we’re resuming the fiber and only in this case we’ll jump into the relevant bytecode index.
- Before a call into another suspendable method we’ll push a snapshot of the current activation frame, including the resume bytecode index; we can do it because we know the structure statically from the analysis phase.
- After a call into another suspendable method we’ll pop the top activation frame and, if resumed, we’ll restore it in the current fiber.
我并沒有更深入的去了解Quasar的實現細節以及調度算法,有興趣的讀者可以翻翻它的代碼。如果你有更深入的剖析,請留下相關的地址,以便我加到參考文檔中。
曾經, 陸陸續續也有一些Java coroutine的實現( coroutine-libraries ), 但是目前來說最好的應該還是Quasar。
Oracle會實現一個官方的纖程庫嗎?目前來說沒有看到這方面的計劃,而且從Java的開發進度上來看,這個特性可能是遙遙無期的,所以目前還只能借助社區的力量,從第三方庫如Quasar中尋找解決方案。
更多的Quasar知識,比如Channel、Actor、Reactive Stream 的使用可以參考官方的文檔,官方也提供了多個 例子 。
Comsat介紹
Comsat又是什么?
Comsat還是Parallel Universe提供的集成Quasar的一套開源庫,可以提供web或者企業級的技術,如HTTP服務和數據庫訪問。
Comsat并不是一套web框架。它并不提供新的API,只是為現有的技術如Servlet、JAX-RS、JDBC等提供Quasar fiber的集成。
它包含非常多的庫,比如Spring、ApacheHttpClient、OkHttp、Undertow、Netty、Kafka等。
性能對比
劉小溪在CSDN上寫了一篇關于Quasar的文章: 次時代Java編程(一):Java里的協程 ,寫的挺好,建議讀者讀一讀。
它參考 Skynet 的測試寫了代碼進行對比,這個測試是并發執行整數的累加:
測試結果是Golang花了261毫秒,Quasar花了612毫秒。其實結果還不錯,但是文中指出這個測試沒有發揮Quasar的性能。因為quasar的性能主要在于阻塞代碼的調度上。
雖然文中加入了排序的功能,顯示Java要比Golang要好,但是我覺得這又陷入了另外一種錯誤的比較, Java的排序算法使用TimSort,排序效果相當好,Go的排序效果顯然比不上Java的實現,所以最后的測試主要測試排序算法上。 真正要體現Quasar的性能還是測試在有阻塞的情況下fiber的調度性能。
HttpClient
話題扯的越來越遠了,拉回來。我最初的目的是要解決的是在第三方服務響應慢的情況下提高系統 A 的吞吐率。最初A是使用200個線程處理業務邏輯,調用第三方服務。因為線程總是被第三方服務阻塞,所以系統A的吞吐率總是很低。
雖然使用Go可以解決這個問題,但是對于系統A的改造比較大,還增加了系統的復雜性。
這正是Quasar fiber適合的場景,如果一個Fiber被阻塞,它可以暫時放棄線程,以便線程可以用來執行其它的Fiber。雖然整個集成系統的吞吐率依然很低,這是無法避免的,但是系統的吞吐率確很高。
Comsat提供了Apache Http Client的實現: FiberHttpClientBuilder :
finalCloseableHttpClient client = FiberHttpClientBuilder.
create(2).// use 2 io threads
setMaxConnPerRoute(concurrencyLevel).
setMaxConnTotal(concurrencyLevel).build();
然后在Fiber中久可以調用:
String response = client.execute(newHttpGet("http://localhost:8080"), BASIC_RESPONSE_HANDLER);
你也可以使用異步的HttpClient:
finalCloseableHttpAsyncClient client = FiberCloseableHttpAsyncClient.wrap(HttpAsyncClients.
custom().
setMaxConnPerRoute(concurrencyLevel).
setMaxConnTotal(concurrencyLevel).
build());
client.start();
Comsat還提供了Jersey Http Client: AsyncClientBuilder.newClient() 。
甚至提供了 Retrofit 、 OkHttp 的實現。
經過測試,雖然隨著系統B、C、D的響應時間的拉長,吞吐率有所降低,但是在latency為100毫秒的時候吞吐率依然能達到9900 requests/秒,可以滿足我們的需求,而我們的代碼改動的比較小。
參考文檔
- http://docs.paralleluniverse.co/quasar/
- http://docs.paralleluniverse.co/comsat/
- http://geek.csdn.net/news/detail/71824
- http://stackoverflow.com/questions/24722233/how-to-use-quasar-with-scala-under-sbt
- http://blog.kazaff.me/2016/05/29/了解協程(coroutine)/
- http://tieba.baidu.com/p/4244920191
- http://javapapers.com/core-java/java-instrumentation/
- https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html
- http://zeroturnaround.com/rebellabs/what-are-fibers-and-why-you-should-care/