Background
In the second article of this series, I will review the source code of hystrix-go
project to understand how to design a circuit breaker
and how to implement it with Golang.
If you’re not familiar with circuit breaker
pattern or hystrix-go
project, please check my previous article about it.
Three service degradation strategies
Hystrix
provides three different service degradation strategies to avoid the cascading failure
happening in the entire system: timeout
, maximum concurrent request numbers
and request error rate
.
- timeout: if the service call doesn’t return response successfully within a predefined time duration, then the fallback logic will run. This strategy is the simplest one.
- maximum concurrent request numbers: when the number of concurrent requests is beyond the threshold, then the fallback logic will handle the following request.
- request error rate:
hystrix
will record the response status of each service call, after the error rate reaches the threshold, the breaker will be open, and the fallback logic will execute before the breaker status changes back to closed.error rate
strategy is the most complex one.
This can be seen from the basic usage of hystrix
as follows:
1 | import ( |
In the above usage case, you can see that timeout
is set to 10 seconds, the maximum request number is 100, and the error rate threshold is 25 percentages.
In the consumer application level, that’s nearly all of the configuration you need to setup. hystrix
will make the magin happen internally.
In this series of articles, I plan to show you the internals of hystrix
by reviewing the source code.
Let’s start from the easy ones: max concurrent requests
and timeout
. Then move on to explore the complex strategy request error rate
.
GoC
Based on the above example, you can see Go
function is the door to the source code of hystrix
, so let’s start from it as follows:
1 | func Go(name string, run runFunc, fallback fallbackFunc) chan error { |
Go
function accept three parameters:
- name: the command name, which is bound to the
circuit
created inside hystrix. - run: a function contains the normal logic which send request to the dependency service.
- fallback: a function contains the fallback logic.
Go
function just wraps run
and fallback
with Context
, which is used to control and cancel goroutine, if you’re not familiar with it then refer to my previous article. Finally it will call GoC
function.
GoC
function goes as follows:
1 | func GoC(ctx context.Context, name string, run runFuncC, fallback fallbackFuncC) chan error { |
I admit it’s complex, but it’s also the core of the entire hystrix
project. Be patient, let’s review it bit by bit carefully.
First of all, the code structure of GoC
function is as follows:
- Construct a new
Command
object, which contains all the information for each call toGoC
function. - Get the
circuit breaker
by name (create it if it doesn’t exist) by callingGetCircuit(name)
function. - Declare condition variable ticketCond and ticketChecked with
sync.Cond
which is used to communicate between goroutines. - Declare function returnTicket. What is a ticket? What does it mean by returnTicket? Let’s discuss it in detail later.
- Declare another function reportAllEvent. This function is critical to
error rate
strategy, and we can leave it for detailed review in the following articles. - Declare an instance of
sync.Once
, which is another interestingsynchronization primitives
provided by golang. - Launch two goroutines, each of which contains many logics too. Simply speaking, the first one contains the logic of sending requests to the target service and the strategy of
max concurrent request number
, and the second one contains thetimeout
strategy. - Return a
channel
type value
Let’s review each of them one by one.
command
command
struct goes as follows, which embeds sync.Mutex and defines several fields:
1 | type command struct { |
Note that command
object iteself doesn’t contain command name information, and its lifecycle is just inside the scope of one GoC
call. It means that the statistic metrics about the service request like error rate
and concurrent request number
are not stored inside command object. Instead, such metrics are stored inside circuit field which is CircuitBreaker
type.
CircuitBreaker
As we mentioned in the workflow of GoC
function, GetCircuit(name)
is called to get or create the circuit breaker
. It is implemented inside circuit.go
file as follows:
1 | func init() { |
The logic is very straightforward. All the circuit breakers are stored in a map object circuitBreakers with the command name as the key.
The newCircuitBreaker
constructor function and CircuitBreaker
struct are as follows:
1 | type CircuitBreaker struct { |
All the fields of CircuitBreaker
are important to understand how the breaker works.
There are two fields that are not simple type need more analysis, include executorPool
and metrics
.
- executorPool: used for
max concurrent request number
strategy, which is just this article’s topic. - metrics: used for
request error rate
strategy, which will be discussed in the next article, all right?
executorPool
We can find executorPool
logics inside the pool.go
file:
1 | type executorPool struct { |
It makes use of golang channel
to realize max concurrent request number
strategy. Note that Tickets
field, which is a buffered channel with capicity of MaxConcurrentRequests is created. And in the following for loop, make the buffered channel full by sending value into the channel until reaching the capacity.
As we have shown above, in the first goroutine of GoC
function, the Tickets
channel is used as follows:
1 | go func() { |
Each call to GoC
function will get a ticket from circuit.executorPool.Tickets channel until no ticket is left, which means the number of concurrent requests reaches the threshold. In that case, the default
case will execute , and the service will be gracefully degraded with fallback logic.
On the other side, after each call to GoC
is done, the ticket need to be sent back to the circuit.executorPool.Tickets, right? Do you remember the returnTicket
function mentioned in above section. Yes, it is just used for this purpose. The returnTicket
function defined in GoC
function goes as follows:
1 | returnTicket := func() { |
It calls executorPool.Return
function:
1 | // Return function in pool.go file |
The design and implementation of Tickets is a great example of golang channel
in the real-world application.
In summary, the max concurrent request number
strategy can be illustrated as follows:
Summary
In this article, max concurrent requests
strategy in hystrix
is reviewed carefully, and I hope you can learn something interesting from it.
But I didn’t cover the detailed logics inside GoC
function, including sync.Cond
, sync.Once
and fallback logics. Let’s review them and timeout
strategy together in the next article.