[原始碼分析] Dynomite 分散式儲存引擎 之 DynoJedisClient(1)
0x00 摘要
前面我們有文章介紹了Amazon Dynamo系統架構 和 NetFlix Dynomite。
我們今天來看看 NetFlix Dynomite 的 Java 客戶端 DynoJedisClient 如何實現。分析客戶端是因為,此客戶端的作用很類似於叢集master,其思路是:java驅動提供多個策略介面,可以用來驅動程式行為調優。包括負載均衡,重試請求,管理節點連線等等。
因為 Dynomite 對於本文來說,過於龐大&底層,而且 DynoJedisClient 與 Dynomite 耦合過於緊密, 所以我們從最簡單的功能點出發看看 DynoJedisClient,於是我們可以想到的功能點是:
- 如何提供基本功能,即提供資料庫連線池;
- 如何管理節點連線;
- 如何拓撲感知;
- 如何負載均衡;
- 如何故障轉移;
- 故障轉移;
所以我們接下來就圍繞這些基本功能點進行分析。
0x01 背景概念
1.1 Amazon Dynamo
亞馬遜在業務發展期間面臨一些問題,主要受限於關係型資料庫的可擴充套件性和高可用性,因此研發了一套新的、基於 KV
儲存模型的資料庫,將之命名為 Dynamo
,其主要採取完全的分散式、去中心化的架構。
相較於傳統的關係型資料庫 MySQL
,Dynamo
的功能目標與之有一些細小的差別,例如: Amazon
的業務場景多數情況並不需要支援複雜查詢,卻要求必要的單節點故障容錯性、資料最終一致性(即犧牲資料強一致優先保障可用性)、較強的可擴充套件性等。
1.2 NetFlix Dynomite
Dynomite 是 NetFlix 對亞馬遜分散式儲存引擎 Dynamo 的一個開源通用實現,它不僅支援基於記憶體的 K/V 資料庫,還支援持久化的 Mysql、BerkeleyDb、LevelDb 等資料庫,並具有簡單、高效、支援跨資料中心的資料複製等優點。
Dynomite 的最終目標是提供資料庫儲存引擎不能提供的簡單、高效、跨資料中心的資料複製功能。目前,Dynomite 已經實現了對 Redis 和 Memcached 的支援。
0x02 Netflix選型思路
Netflix選擇Dynomite,是因為:
-
其具有效能,多資料中心複製和高可用性的特點;
-
Dynomite提供分片和可插拔的資料儲存引擎,允許在資料需求增加垂直和水平擴充套件;
-
Dynomite在Redis之上提供了高可用性、對等複製以及一致性等特性,用於構建分散式叢集佇列。
-
Dyno為持久連線提供連線池;
-
Dyno可以為連線池配置為拓撲感知;
-
故障轉移:Dyno為應用程式提供特定的本地機架,us-east-1a的客戶端將連線到相同區域的Dynomite/Redis節點,除非該節點不可用,在這種情況下該客戶端將進行故障轉移。這個屬性被用於通過區域劃分佇列。
Dynomite對於本文來說,過於底層。
所以我們重點就看看 DynoJedisClient 如何實現後面幾點,當然,這幾點其實也無法脫離Dynomite,我們只是力爭剝離出來。
0x03 基礎知識
3.1 Data Center
Data Center 是由多個Rack組成的邏輯集合。
Data Center 可以是一個機房或者一個區域的裝置組合。
3.2 Rack
這是一個邏輯集合,有多個彼此臨近node的組成。比如一個機架上的所有物理機器。可簡單的理解為存放伺服器的機櫃。
資料中心與機架是什麼關係呢?N:1,1:N,M:N。
- 如果只需要幾臺伺服器就能滿足業務需求,這些伺服器至少有2個資料中心,那這種情況下多個資料中心可以放在1個機架上,不過這種情況對資料災備來說是不太保險的。
- 第2種情況是1個資料中心相當於1個機房,那機房裡會有多個機架。
- 第3種情況M:N為多個機房的多個資料中心置於多個機架上。
3.2 Rings and Tokens
由叢集管理的資料就是一個環。環中的每個節點被分配一個或多個由token描述的資料範圍,確定在環中的位置。
Token是用於標識每個分割槽的64位整數ID,範圍是-2^63 -- 2^63-1。通過hash演算法計算partition key的hash值,以此確定存放在哪個節點。
Token也決定了每個節點儲存的資料的分佈範圍,每個節點儲存的資料的key在(前一個節點Token,本節點Token]的半開半閉區間內,所有的節點形成一個首尾相接的環。
0x04 需求 & 思路
因為要為上層遮蔽資訊,所以 DynoJedisClient 就需要應對各種複雜資訊,需要對系統有深刻的瞭解,比如:
- 如何維護連線,為持久連線提供連線池;
- 如何維護拓撲;
- 如何負載均衡;
- 如何故障轉移;
- 如何自動重試及發現,比如自動重試掛掉的主機。自動發現叢集中的其他主機。
- 如何監控底層機架狀態;
因此,DynoJedisClient 的思路是:java驅動提供多個策略介面,可以用來驅動程式行為調優。包括負載均衡,重試請求,管理節點連線等等。
0x05 使用
示例程式碼如下:
public static void main(String[] args) throws IOException {
final String clusterName = args[0];
int version = Integer.parseInt(args[1]);
final DynoQueueDemo demo = new DynoQueueDemo(clusterName, "us-east-1e");
Properties props = new Properties();
props.load(DynoQueueDemo.class.getResourceAsStream("/demo.properties"));
for (String name : props.stringPropertyNames()) {
System.setProperty(name, props.getProperty(name));
}
try {
demo.initWithRemoteClusterFromEurekaUrl(args[0], 8102, false);
if (version == 1) {
demo.runSimpleV1Demo(demo.client);
} else if (version == 2) {
demo.runSimpleV2QueueDemo(demo.client);
}
Thread.sleep(10000);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
demo.stop();
logger.info("Done");
}
}
以及輔助函式:
public void initWithRemoteClusterFromEurekaUrl(final String clusterName, final int port, boolean lock) throws Exception {
initWithRemoteCluster(clusterName, getHostsFromDiscovery(clusterName), port, lock);
}
private void initWithRemoteCluster(String clusterName, final List<Host> hosts, final int port, boolean lock) throws Exception {
final HostSupplier clusterHostSupplier = () -> hosts;
if (lock)
initDynoLockClient(clusterHostSupplier, null, "test", clusterName);
else
init(clusterHostSupplier, port, null);
}
public void initDynoLockClient(HostSupplier hostSupplier, TokenMapSupplier tokenMapSupplier, String appName,
String clusterName) {
dynoLockClient = new DynoLockClient.Builder().withApplicationName(appName)
.withDynomiteClusterName(clusterName)
.withTimeoutUnit(TimeUnit.MILLISECONDS)
.withTimeout(10000)
.withHostSupplier(hostSupplier)
.withTokenMapSupplier(tokenMapSupplier).build();
}
0x06 配置
在 DynoJedisClient 之中,有如下重要配置類。
6.1 預設配置
ConnectionPoolConfigurationImpl主要是提供預設配置。
public class ConnectionPoolConfigurationImpl implements ConnectionPoolConfiguration {
// DEFAULTS
private static final LoadBalancingStrategy DEFAULT_LB_STRATEGY = LoadBalancingStrategy.TokenAware;
private static final CompressionStrategy DEFAULT_COMPRESSION_STRATEGY = CompressionStrategy.NONE;
private HostSupplier hostSupplier;
private TokenMapSupplier tokenSupplier;
private HostConnectionPoolFactory hostConnectionPoolFactory;
private HashPartitioner hashPartitioner;
private LoadBalancingStrategy lbStrategy = DEFAULT_LB_STRATEGY;
private CompressionStrategy compressionStrategy = DEFAULT_COMPRESSION_STRATEGY;
}
6.2 策略配置
ArchaiusConnectionPoolConfiguration最主要是提供了若干策略,包括負載,壓縮,重試:
- LoadBalancingStrategy parseLBStrategy(String propertyPrefix) 是負載策略;
- CompressionStrategy parseCompressionStrategy(String propertyPrefix) 是壓縮策略;
- RetryPolicyFactory parseRetryPolicyFactory(String propertyPrefix) 是重試策略;
具體如下:
public class ArchaiusConnectionPoolConfiguration extends ConnectionPoolConfigurationImpl {
......
private final LoadBalancingStrategy loadBalanceStrategy;
private final CompressionStrategy compressionStrategy;
private final ErrorRateMonitorConfig errorRateConfig;
private final RetryPolicyFactory retryPolicyFactory;
private final DynamicBooleanProperty failOnStartupIfNoHosts;
private final DynamicIntProperty lockVotingSize;
......
}
0x07 定義
DynoJedisClient 定義如下,我們可以看到最重要的成員變數就是連線池ConnectionPool。
public class DynoJedisClient implements JedisCommands, BinaryJedisCommands, MultiKeyCommands,ScriptingCommands, MultiKeyBinaryCommands, DynoJedisCommands {
private final String appName;
private final String clusterName;
private final ConnectionPool<Jedis> connPool;
private final AtomicReference<DynoJedisPipelineMonitor> pipelineMonitor = new AtomicReference<DynoJedisPipelineMonitor>();
protected final DynoOPMonitor opMonitor;
protected final ConnectionPoolMonitor cpMonitor;
}
0x08 邏輯連線池
因為 DynoJedisClient 最主要是管理連線池,所以我們首先介紹 邏輯連線池 ConnectionPoolImpl。
連線池層為應用程式抽象所有連線管理。在這裡,我們可以配置所有內容,例如指定池選項,負載平衡策略,重試策略或預設一致性級別。
ConnectionPoolImpl 是核心類,其主要功能是:
- 對於從HostSupplier獲得的各種HostConnectionPool進行維護,形成一個HostConnectionPool集合;
- 對於HostSupplier檢測到的hosts,進行新增刪除;
- 從HostConnectionPool提取Connection,進行Operation的執行;
- 在執行Operation時,採用HostSelectionStrategy,比如:basically Round Robin 或者 TokenAware策略;
- 使用health check monitor來進行錯誤率跟蹤。health check monitor可以決定重用HostConnectionPool,以及fallback到remote資料中心的HostConnectionPools執行;
- 使用RetryPolicy來執行operation;
具體定義如下:
public class ConnectionPoolImpl<CL> implements ConnectionPool<CL>, TopologyView {
private final ConcurrentHashMap<Host, HostConnectionPool<CL>> cpMap = new ConcurrentHashMap<Host, HostConnectionPool<CL>>();
private final ConnectionPoolHealthTracker<CL> cpHealthTracker;
private final HostConnectionPoolFactory<CL> hostConnPoolFactory;
private final ConnectionFactory<CL> connFactory;
private final ConnectionPoolConfiguration cpConfiguration;
private final ConnectionPoolMonitor cpMonitor;
private final ScheduledExecutorService idleThreadPool = Executors.newSingleThreadScheduledExecutor();
private final HostsUpdater hostsUpdater;
private final ScheduledExecutorService connPoolThreadPool = Executors.newScheduledThreadPool(1);
private HostSelectionWithFallback<CL> selectionStrategy;
private Type poolType;
}
此時邏輯如下:
+------------------------+
|DynoJedisClient |
| |
| | +------------------------+
| | | |
| connPool +--------------> | ConnectionPoolImpl |
| | | |
| | +------------------------+
+------------------------+
8.1 啟動
連線池 啟動邏輯是:
- 利用hostsUpdater來獲取到的host進行配置新增;
- 啟用health check monitor來進行錯誤率跟蹤;
具體如下:
@Override
public Future<Boolean> start() throws DynoException {
HostSupplier hostSupplier = cpConfiguration.getHostSupplier();
HostStatusTracker hostStatus = hostsUpdater.refreshHosts();
cpMonitor.setHostCount(hostStatus.getHostCount());
Collection<Host> hostsUp = hostStatus.getActiveHosts();
final ExecutorService threadPool = Executors.newFixedThreadPool(Math.max(10, hostsUp.size()));
final List<Future<Void>> futures = new ArrayList<Future<Void>>();
// 利用hostsUpdater來獲取到的host進行配置新增
for (final Host host : hostsUp) {
// Add host connection pool, but don't init the load balancer yet
futures.add(threadPool.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
addHost(host, false);
return null;
}
}));
}
// 啟用health check monitor來進行錯誤率跟蹤
boolean success = started.compareAndSet(false, true);
if (success) {
selectionStrategy = initSelectionStrategy();
cpHealthTracker.start();
connPoolThreadPool.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
HostStatusTracker hostStatus = hostsUpdater.refreshHosts();
cpMonitor.setHostCount(hostStatus.getHostCount());
Logger.debug(hostStatus.toString());
updateHosts(hostStatus.getActiveHosts(), hostStatus.getInactiveHosts());
}
}, 15 * 1000, 30 * 1000, TimeUnit.MILLISECONDS);
MonitorConsole.getInstance().registerConnectionPool(this);
registerMonitorConsoleMBean(MonitorConsole.getInstance());
}
return getEmptyFutureTask(true);
}
8.2 配置Host
啟動過程中,新增host邏輯如下:
- 依據host獲取HostConnectionPool;
- 把HostConnectionPool加入到集合;
- 把 host,HostConnectionPool加入到選擇策略selectionStrategy;
- 依據host設定health check monitor;
具體如下:
public boolean addHost(Host host, boolean refreshLoadBalancer) {
HostConnectionPool<CL> connPool = cpMap.get(host);
final HostConnectionPool<CL> hostPool = hostConnPoolFactory.createHostConnectionPool(host, this);
HostConnectionPool<CL> prevPool = cpMap.putIfAbsent(host, hostPool);
if (prevPool == null) {
// This is the first time we are adding this pool.
try {
int primed = hostPool.primeConnections();
if (hostPool.isActive()) {
if (refreshLoadBalancer) {
selectionStrategy.addHost(host, hostPool);
}
cpHealthTracker.initializePingHealthchecksForPool(hostPool);
cpMonitor.hostAdded(host, hostPool);
} else {
cpMap.remove(host);
}
return primed > 0;
} catch (DynoException e) {
cpMap.remove(host);
return false;
}
}
}
8.3 獲取HostConnectionPool
關於獲取HostConnectionPool,有同步和非同步 兩種實現方式,具體如下。
private class SyncHostConnectionPoolFactory implements HostConnectionPoolFactory<CL> {
@Override
public HostConnectionPool<CL> createHostConnectionPool(Host host, ConnectionPoolImpl<CL> parentPoolImpl) {
return new HostConnectionPoolImpl<CL>(host, connFactory, cpConfiguration, cpMonitor);
}
}
private class AsyncHostConnectionPoolFactory implements HostConnectionPoolFactory<CL> {
@Override
public HostConnectionPool<CL> createHostConnectionPool(Host host, ConnectionPoolImpl<CL> parentPoolImpl) {
return new SimpleAsyncConnectionPoolImpl<CL>(host, connFactory, cpConfiguration, cpMonitor);
}
}
8.4 執行
邏輯連線池 有兩種執行方式:executeWithRing 與 executeWithFailover。
-
executeWithRing使用較少,所以不詳細介紹。
-
executeWithFailover 是 利用selectionStrategy獲取Connection,在此Connection之上進行執行。如果失敗就各種重試。
public <R> OperationResult<R> executeWithFailover(Operation<CL, R> op) throws DynoException {
RetryPolicy retry = cpConfiguration.getRetryPolicyFactory().getRetryPolicy();
retry.begin();
do {
Connection<CL> connection = null;
try {
connection = selectionStrategy.getConnectionUsingRetryPolicy(op,
cpConfiguration.getMaxTimeoutWhenExhausted(), TimeUnit.MILLISECONDS, retry);
updateConnectionContext(connection.getContext(), connection.getHost());
OperationResult<R> result = connection.execute(op);
// Add context to the result from the successful execution
result.setNode(connection.getHost()).addMetadata(connection.getContext().getAll());
retry.success();
cpMonitor.incOperationSuccess(connection.getHost(), System.currentTimeMillis() - startTime);
return result;
} finally {
if (connection != null) {
if (connection.getLastException() != null
&& connection.getLastException() instanceof FatalConnectionException) {
connection.getParentConnectionPool().recycleConnection(connection);
// note - don't increment connection closed metric here;
// it's done in closeConnection
} else {
connection.getContext().reset();
connection.getParentConnectionPool().returnConnection(connection);
}
}
}
} while (retry.allowRetry());
throw lastException;
}
此時邏輯如下:
+----------------------+
+-------------------+ |ConnectionPoolImpl |
|DynoJedisClient | | |
| | | | +--------------+
| | | hostsUpdater +--------> | HostSupplier |
| | | | +--------------+
| connPool +---------> | |
| | | | +--------------------------+
| | | cpMap +--------> |[Host, HostConnectionPool]|
+-------------------+ | | | + |
+----------------------+ | | |
+--------------------------+
|
|
|
v
+---------------+-----+
| |
| HostConnectionPool |
| |
+---------------------+
0x09 具體連線池
HostConnectionPool 是具體連線池實現,此類為每一個Host節點維護一個有效連線池。
具體是:
- HostConnectionPool 使用 LinkedBlockingQueue availableConnections 來維護所有有效連線,當client需要一個連線,需要從queue中提取。
- 所以,availableConnections 就是有效連線池。
- availableConnections 之中每一個 連線就是一個 Connection;
- 這個 Connection (JedisConnection)是通過 JedisConnectionFactory 建立的;
- 另外,每一個 JedisConnection 裡面有:
- HostConnectionPool
hostPool; - Jedis jedisClient;
- HostConnectionPool
具體如下:
public class HostConnectionPoolImpl<CL> implements HostConnectionPool<CL> {
// The connections available for this connection pool
private final LinkedBlockingQueue<Connection<CL>> availableConnections = new LinkedBlockingQueue<Connection<CL>>();
// Private members required by this class
private final Host host;
private final ConnectionFactory<CL> connFactory;
private final ConnectionPoolConfiguration cpConfig;
private final ConnectionPoolMonitor monitor;
// states that dictate the behavior of the pool
// cp not inited is the starting state of the pool. The pool will not allow connections to be borrowed in this state
private final ConnectionPoolState<CL> cpNotInited = new ConnectionPoolNotInited();
// cp active is where connections of the pool can be borrowed and returned
private final ConnectionPoolState<CL> cpActive = new ConnectionPoolActive(this);
// cp reconnecting is where connections cannot be borrowed and all returning connections will be shutdown
private final ConnectionPoolState<CL> cpReconnecting = new ConnectionPoolReconnectingOrDown();
// similar to reconnecting
private final ConnectionPoolState<CL> cpDown = new ConnectionPoolReconnectingOrDown();
// The thread safe reference to the pool state
private final AtomicReference<ConnectionPoolState<CL>> cpState = new AtomicReference<ConnectionPoolState<CL>>(cpNotInited);
}
9.1 生成Connection
首先我們要看看 如何生成 Connection,大致就是從 connFactory 中直接獲取,然後執行監控等相應操作。
@Override
public Connection<CL> createConnection() {
try {
Connection<CL> connection;
if (cpConfig.isConnectToDatastore()) {
// 具體建立連線操作
connection = connFactory.createConnectionWithDataStore(pool);
} else if (cpConfig.isConnectionPoolConsistencyProvided()) {
connection = connFactory.createConnectionWithConsistencyLevel(pool, cpConfig.getConnectionPoolConsistency());
} else {
connection = connFactory.createConnection(pool);
}
connection.open();
availableConnections.add(connection);
monitor.incConnectionCreated(host);
numActiveConnections.incrementAndGet();
return connection;
}
}
9.2 JedisConnectionFactory
JedisConnectionFactory 的 createConnectionWithDataStore 函式執行了具體 建立連線操作,涉及到 Jedis 很多朋友應該都很熟悉。
簡略版程式碼如下:
public class JedisConnectionFactory implements ConnectionFactory<Jedis> {
private final OperationMonitor opMonitor;
private final SSLSocketFactory sslSocketFactory;
public JedisConnectionFactory(OperationMonitor monitor, SSLSocketFactory sslSocketFactory) {
this.opMonitor = monitor;
this.sslSocketFactory = sslSocketFactory;
}
@Override
public Connection<Jedis> createConnectionWithDataStore(HostConnectionPool<Jedis> pool) {
return new JedisConnection(pool, true);
}
// TODO: raghu compose redisconnection with jedisconnection in it
public class JedisConnection implements Connection<Jedis> {
private final HostConnectionPool<Jedis> hostPool;
private final Jedis jedisClient;
public JedisConnection(HostConnectionPool<Jedis> hostPool, boolean connectDataStore) {
this.hostPool = hostPool;
Host host = hostPool.getHost();
int port = connectDataStore ? host.getDatastorePort() : host.getPort();
if (sslSocketFactory == null) {
JedisShardInfo shardInfo = new JedisShardInfo(host.getHostAddress(), port,
hostPool.getConnectionTimeout(), hostPool.getSocketTimeout(), Sharded.DEFAULT_WEIGHT);
jedisClient = new Jedis(shardInfo);
} else {
JedisShardInfo shardInfo = new JedisShardInfo(host.getHostAddress(), port,
hostPool.getConnectionTimeout(), hostPool.getSocketTimeout(), Sharded.DEFAULT_WEIGHT,
true, sslSocketFactory, new SSLParameters(), null);
jedisClient = new Jedis(shardInfo);
}
}
@Override
public HostConnectionPool<Jedis> getParentConnectionPool() {
return hostPool;
}
public Jedis getClient() {
return jedisClient;
}
}
}
此時邏輯如下:
+----------------------+
+-------------------+ |ConnectionPoolImpl |
|DynoJedisClient | | |
| | | | +--------------+
| | | hostsUpdater +--------> | HostSupplier |
| | | | +--------------+
| connPool +---------> | |
| | | | +--------------------------+
| | | cpMap +--------> |[Host, HostConnectionPool]|
+-------------------+ | | | + |
+----------------------+ | | |
+--------------------------+
|
|
+-----------------------------+ |
| JedisConnectionFactory | v
| | +---------------+-------------------------------------------+
| | createConnectionWithDataStore | HostConnectionPool |
| | | |
| sslSocketFactory | <------------------------------------------------+ connFactory Host |
| | | |
| | | LinkedBlockingQueue<Connection<CL<> availableConnections |
+-----------------------------+ | |
+------------------------------+----------------------------+
+ ^
| +----------------------------------------+ |
| |JedisConnection | |
| | | |
| return | | return |
| | HostConnectionPool<Jedis> hostPool | |
+---------------> | | +--------------------------------+
| Jedis(shardInfo) jedisClient |
| |
+----------------------------------------+
手機上如下:
9.3 獲取Connection
使用者使用 borrowConnection 來得到 連線,並且做監控。
@Override
public Connection<CL> borrowConnection(int duration, TimeUnit unit) {
// Start recording how long it takes to get the connection - for insight/metrics
long startTime = System.nanoTime() / 1000;
Connection<CL> conn = null;
// wait on the connection pool with a timeout
conn = availableConnections.poll(duration, unit);
long delay = System.nanoTime() / 1000 - startTime;
monitor.incConnectionBorrowed(host, delay);
}
0x10 拓撲
這裡拓撲主要指的是token環,我們再複習下概念。
在 Dynomite 之中,由叢集管理的資料就是一個環。環中的每個節點被分配一個或多個由token描述的資料範圍,toekn 可以確定在環中的位置。
Token是用於標識每個分割槽的64位整數ID,範圍是-2^63 -- 2^63-1。通過hash演算法計算partition key的hash值,以此確定存放在哪個節點。
Token決定了每個節點儲存的資料的分佈範圍,每個節點儲存的資料的key在(前一個節點Token,本節點Token]的半開半閉區間內,所有的節點形成一個首尾相接的環。
10.1 只讀檢視
TopologyView代表了伺服器拓撲的只讀檢視。
public interface TopologyView {
/**
* Retrieves a read-only view of the server topology
*
* @return An unmodifiable map of server-id to list of token status
*/
Map<String, List<TokenPoolTopology.TokenStatus>> getTopologySnapshot();
/**
* Returns the token for the given key.
*
* @param key The key of the record stored in dynomite
* @return Long The token that owns the given key
*/
Long getTokenForKey(String key);
}
ConnectionPoolImpl 實現了 TopologyView,即 implements TopologyView。
所以 ConnectionPoolImpl 本身就是一個 TopologyView。
public class ConnectionPoolImpl<CL> implements ConnectionPool<CL>, TopologyView {
public TokenPoolTopology getTopology() {
return selectionStrategy.getTokenPoolTopology();
}
@Override
public Map<String, List<TokenPoolTopology.TokenStatus>> getTopologySnapshot() {
return Collections.unmodifiableMap(selectionStrategy.getTokenPoolTopology().getAllTokens());
}
@Override
public Long getTokenForKey(String key) {
if (cpConfiguration
.getLoadBalancingStrategy() == ConnectionPoolConfiguration.LoadBalancingStrategy.TokenAware) {
return selectionStrategy.getTokenForKey(key);
}
return null;
}
}
在 DynoJedisClient 中獲取 TopologyView 就是直接 獲取了 ConnectionPoolImpl。
public TopologyView getTopologyView() {
return this.getConnPool();
}
所以此時邏輯圖上加入了 TopologyView 。
+----------------------+
+-------------------+ |ConnectionPoolImpl |
|DynoJedisClient | | |
| | | | +--------------+
| | | hostsUpdater +--------> | HostSupplier |
| | | | +--------------+
| connPool +---------> | |
| | | | +--------------------------+
| TopologyView +------> | cpMap +--------> |[Host, HostConnectionPool]|
| | | | | + |
+-------------------+ +----------------------+ | | |
+--------------------------+
|
|
+-----------------------------+ |
| JedisConnectionFactory | v
| | +---------------+-------------------------------------------+
| | createConnectionWithDataStore | HostConnectionPool |
| | | |
| sslSocketFactory | <------------------------------------------------+ connFactory Host |
| | | |
| | | LinkedBlockingQueue<Connection<CL<> availableConnections |
+-----------------------------+ | |
+------------------------------+----------------------------+
+ ^
| +----------------------------------------+ |
| |JedisConnection | |
| | | |
| return | | return |
| | HostConnectionPool<Jedis> hostPool | |
+---------------> | | +--------------------------------+
| Jedis(shardInfo) jedisClient |
| |
+----------------------------------------+
手機如下:
10.2 具體實現
TokenPoolTopology 屬於 拓撲 的具體實現。
getTopologySnapshot就是return map
。就是得到對應了所有 rack 的 TokenStatus,這就是拓撲。
其實大家仔細想想就可以理解,拓撲不就是 “當前所有機架上分別有哪些東西,這些東西是什麼狀態" 的一個邏輯集合嘛。
具體定義如下,其核心成員是兩個:
- map 可以理解為 rack 作為key,value 是一個list,即 "該 rack 上對應的 token status 被整理成 list";
- rackTokenHostMap 可以理解為 rack 作為 key,value 是一個map,即 "該 rack 上的 token status <---> host 之間的關係被整理成一個 map";
這樣就有兩個不同維度可以分別處理這些 token了。
public class TokenPoolTopology {
private final ConcurrentHashMap<String, List<TokenStatus>> map = new ConcurrentHashMap<String, List<TokenStatus>>();
private final ConcurrentHashMap<String, Map<Long, Host>> rackTokenHostMap = new ConcurrentHashMap<String, Map<Long, Host>>();
public ConcurrentHashMap<String, List<TokenStatus>> getAllTokens() {
return map;
}
public void addToken(String rack, Long token, HostConnectionPool<?> hostPool) {
List<TokenStatus> list = map.get(rack);
if (list == null) {
list = new ArrayList<TokenStatus>();
map.put(rack, list);
}
list.add(new TokenStatus(token, hostPool));
}
public void addHostToken(String rack, Long token, Host host) {
Map<Long, Host> tokenHostMap = rackTokenHostMap.get(rack);
if (tokenHostMap == null) {
tokenHostMap = new HashMap<>();
rackTokenHostMap.put(rack, tokenHostMap);
}
tokenHostMap.put(token, host);
}
}
10.3 如何使用
TokenPoolTopology 具體在 ConnectionPoolImpl 和 HostSelectionWithFallback 都有使用。
10.3.1 ConnectionPoolImpl
ConnectionPoolImpl中如下處理,或者直接返回由上層再處理,或者就是直接返回 TokenPoolTopology 之中的所有 token 給上層:
public TokenPoolTopology getTopology() {
return selectionStrategy.getTokenPoolTopology();
}
public Map<String, List<TokenPoolTopology.TokenStatus>> getTopologySnapshot() {
return Collections.unmodifiableMap(selectionStrategy.getTokenPoolTopology().getAllTokens());
}
10.3.2 HostSelectionWithFallback
HostSelectionWithFallback中也有TokenPoolTopology的使用,只是用來 failover/fallback使用。
public class HostSelectionWithFallback<CL> {
// Represents the *initial* topology from the token supplier. This does not affect selection of a host connection
// pool for traffic. It only affects metrics such as failover/fallback
private final AtomicReference<TokenPoolTopology> topology = new AtomicReference<>(null);
}
HostSelectionWithFallback中 也利用 host tokens 來建立或者更新已有的 TokenPoolTopology。
/**
* Create token pool topology from the host tokens
*
* @param allHostTokens
* @return tokenPoolTopology with the host information
*/
public TokenPoolTopology createTokenPoolTopology(List<HostToken> allHostTokens) {
TokenPoolTopology topology = new TokenPoolTopology(replicationFactor.get());
for (HostToken hostToken : allHostTokens) {
String rack = hostToken.getHost().getRack();
topology.addHostToken(rack, hostToken.getToken(), hostToken.getHost());
}
updateTokenPoolTopology(topology);
return topology;
}
至此,連線管理和拓撲感知部分已經分析完畢,下文將繼續分析自動發現和故障轉移。