業務模塊介紹 現在我們對整體的業務進行介紹以及演示 5. 全鏈路整體架構 上面介紹了為什麼需要全鏈路壓測,下麵來看下全鏈路壓測的整體架構。 整體架構如下主要是對壓測客戶端的壓測數據染色,全鏈路中間件識別出染色數據,並將正常數據和壓測數據區分開,進行數據隔離,這裡主要涉及到mysql資料庫,Rab ...
業務模塊介紹
現在我們對整體的業務進行介紹以及演示
5. 全鏈路整體架構
上面介紹了為什麼需要全鏈路壓測,下麵來看下全鏈路壓測的整體架構。
整體架構如下主要是對壓測客戶端的壓測數據染色,全鏈路中間件識別出染色數據,並將正常數據和壓測數據區分開,進行數據隔離,這裡主要涉及到mysql資料庫,RabbitMQ,Redis,還需要處理因為hystrix線程池不能通過ThreadLocal傳遞染色表示的問題。
5.1 需要應對的問題
5.1.1 業務問題
如何開展全鏈路壓測?在說這個問題前,我們先考慮下,全鏈路壓測有哪些問題比較難解決。
-
涉及的系統太多,牽扯的開發人員太多
在壓測過程中,做一個全鏈路的壓測一般會涉及到大量的系統,在整個壓測過程中,光各個產品的人員協調就是一個比較大的工程,牽扯到太多的產品經理和開發人員,如果公司對全鏈路壓測早期沒有足夠的重視,那麼這個壓測工作是非常難開展的。
-
模擬的測試數據和訪問流量不真實
在壓測過程中經常會遇到壓測後得到的數據不准確的問題,這就使得壓測出的數據參考性不強,為什麼會產生這樣的問題?主要就是因為壓測的環境可能和生成環境存在誤差、參數存在不一樣的地方、測試數據存在不一樣的地方這些因素綜合起來導致測試結果的不可信。
-
壓測生產數據未隔離,影響生產環境
在全鏈路壓測過程中,壓測數據可能會影響到生產環境的真實數據,舉個例子,電商系統在生產環境進行全鏈路壓測的時候可能會有很多壓測模擬用戶去下單,如果不做處理,直接下單的話會導致系統一下子會產生很多廢訂單,從而影響到庫存和生產訂單數據,影響到日常的正常運營。
5.1.2 技術問題
5.1.2.1 探針的性能消耗
APM組件服務的影響應該做到足夠小。
服務調用埋點本身會帶來性能損耗,這就需要調用跟蹤的低損耗,實際中還會通過配置採樣率的方式,選擇一部分請求去分析請求路徑。在一些高度優化過的服務,即使一點點損耗也會很容易察覺到,而且有可能迫使線上服務的部署團隊不得不將跟蹤系統關停。
5.1.2.2 代碼的侵入性
即也作為業務組件,應當儘可能少入侵或者無入侵其他業務系統,對於使用方透明,減少開發人員的負擔。
對於應用的程式員來說,是不需要知道有跟蹤系統這回事的。如果一個跟蹤系統想生效,就必須需要依賴應用的開發者主動配合,那麼這個跟蹤系統也太脆弱了,往往由於跟蹤系統在應用中植入代碼的bug或疏忽導致應用出問題,這樣才是無法滿足對跟蹤系統“無所不在的部署”這個需求。
5.1.2.3 可擴展性
一個優秀的調用跟蹤系統必須支持分散式部署,具備良好的可擴展性。能夠支持的組件越多當然越好。或者提供便捷的插件開發API,對於一些沒有監控到的組件,應用開發者也可以自行擴展。
5.1.2.4 數據的分析
數據的分析要快 ,分析的維度儘可能多。跟蹤系統能提供足夠快的信息反饋,就可以對生產環境下的異常狀況做出快速反應。分析的全面,能夠避免二次開發。
5.2 全鏈路壓測核心技術
上面從總體架構層面分析了全鏈路壓測的核心,下麵就分析下全鏈路壓測用到的核心技術點
5.2.1 全鏈路流量染色
做到微服務和中間件的染色標誌的穿透
通過壓測平臺對輸出的壓力請求打上標識,在訂單系統中提取壓測標識,確保完整的程式上下文都持有該標識,並且能夠穿透微服務以及各種中間件,比如 MQ,hystrix,Fegin等。
5.2.2 全鏈路服務監控
需要能夠實時監控服務的運行狀況以及分析服務的調用鏈,我們採用skywalking進行服務監控和壓測分析
5.2.3 全鏈路日誌隔離
做到日誌隔離,防止污染生產日誌
當訂單系統向磁碟或外設輸出日誌時,若流量是被標記的壓測流量,則將日誌隔離輸出,避免影響生產日誌。
5.2.4 全鏈路風險熔斷
流量控制,防止流量超載,導致集群不可用
當訂單系統訪問會員系統時,通過RPC協議延續壓測標識到會員系統,兩個系統之間服務通訊將會有白黑名單開關來控制流量流入許可。該方案設計可以一定程度上避免下游系統出現瓶頸或不支持壓測所帶來的風險,這裡可以採用Sentinel來實現風險熔斷。
5.3 全鏈路數據隔離
對各種存儲服務以及中間件做到數據隔離,方式數據污染
2.3.1 資料庫隔離
當會員系統訪問資料庫時,在持久化層同樣會根據壓測標識進行路由訪問壓測數據表。數據隔離的手段有多種,比如影子庫、影子表,或者影子數據,三種方案的模擬度會有一定的差異,他們的對比如下。
隔離性 | 相容性 | 安全級別 | 技術難度 | |
---|---|---|---|---|
影子庫 | 高 | 高 | 高 | 高 |
影子表 | 中 | 低 | 中 | 中 |
影子數據 | 低 | 低 | 低 | 低 |
5.3.2 消息隊列隔離
當我們生產的消息扔到MQ之後,接著讓消費者進行消費,這個沒有問題,壓測的數據不能夠直接扔到MQ中的,因為它會被正常的消費者消費到的,要做好數據隔離,方案有隊列隔離,消息隔離,他們對比如下。
隔離性 | 相容性 | 安全級別 | 技術難度 | |
---|---|---|---|---|
隊列隔離 | 高 | 好 | 高 | 高 |
消息隔離 | 低 | 低 | 低 | 中 |
5.3.3 Redis 隔離
通過 key 值來區分,壓測流量的 key 值加統一尾碼,通過改造RedisTemplate來實現key的路由。
框架實現
6.1 流量染色方案
上面分析了從整體分析了全鏈路壓測用的的核心技術,下麵就來實現第一個流量染色。
6.1.1 流量識別
要想壓測的流量和數據不影響線上真實的生產數據,就需要線上的集群能識別出壓測的流量,只要能識別出壓測請求的流量,那麼流量觸發的讀寫操作就很好統一去做隔離了。
全鏈路壓測發起的都是Http的請求,只需要要請求頭上添加統一的壓測請求頭。
通過在請求協議中添加壓測請求的標識,在不同服務的相互調用時,一路透傳下去,這樣每一個服務都能識別出壓測的請求流量,這樣做的好處是與業務完全的解耦,只需要應用框架進行感知,對業務方代碼無侵入。
6.1.2 MVC接收數據
客戶端傳遞過來的數據可以通過獲取Header的方式獲取到,並將其設置進當前的ThreadLocal,交給後面的方法使用。
6.1.2.1 MVC攔截器實現
/**
* 鏈路跟蹤Request設置值
*/
public class MvcWormholeWebInterceptor implements WebRequestInterceptor {
@Override
public void preHandle(WebRequest webRequest) {
//失效上下文,解決Tomcat線程復用問題
WormholeContextHolder.invalidContext();
String wormholeValue = webRequest.getHeader(WormholeContextHolder.WORMHOLE_REQUEST_MARK);
if (StringUtils.isNotEmpty(wormholeValue)) {
WormholeContextHolder.setContext(new WormholeContext(wormholeValue));
}
}
@Override
public void postHandle(WebRequest webRequest, ModelMap modelMap) throws Exception {
}
@Override
public void afterCompletion(WebRequest webRequest, Exception e) throws Exception {
}
}
6.1.2.2 Tomcat線程復用問題
tomcat預設使用線程池來管理線程,一個請求過來,如果線程池裡面有空閑的線程,那麼會線上程池裡面取一個線程來處理該請求,一旦該線程當前在處理請求,其他請求就不會被分配到該線程上,直到該請求處理完成。請求處理完成後,會將該線程重新加入線程池,因為是通過線程池復用線程,就會如果線程內部的ThreadLocal沒有清除就會出現問題,需要新的請求進來的時候,清除ThreadLocal。
6.1.3 Fegin傳遞傳遞染色標識
我們項目的微服務是使用Fegin來實現遠程調用的,跨微服務傳遞染色標識是通過MVC攔截器獲取到請求Header的染色標識,並放進ThreadLocal中,然後交給Fegin攔截器在發送請求之前從ThreadLocal中獲取到染色標識,並放進Fegin構建請求的Header中,實現微服務之間的火炬傳遞。
6.1.3.1 代碼實現
public class WormholeFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
WormholeContext wormholeContext = WormholeContextHolder.getContext();
if (null != wormholeContext) {
requestTemplate.header(WormholeContextHolder.WORMHOLE_REQUEST_MARK, wormholeContext.toString());
}
}
}
6.1.4 Hystrix傳遞染色標識
6.1.4.1 Hystrix隔離技術
Hystrix 實現資源隔離,主要有兩種技術:
信號量
信號量的資源隔離只是起到一個開關的作用,比如,服務 A 的信號量大小為 10,那麼就是說它同時只允許有 10 個 tomcat 線程來訪問服務 A,其它的請求都會被拒絕,從而達到資源隔離和限流保護的作用。
線程池
線程池隔離技術,是用 Hystrix 自己的線程去執行調用;而信號量隔離技術,是直接讓 tomcat 線程去調用依賴服務。信號量隔離,只是一道關卡,信號量有多少,就允許多少個 tomcat 線程通過它,然後去執行。
6.1.4.2 Hystrix穿透
如果使用線程池模式,那麼存在一個ThreadLocal變數跨線程傳遞的問題,即在主線程的ThreadLocal變數,無法線上程池中使用,不過Hystrix內部提供瞭解決方案。
封裝Callable任務
public final class DelegatingWormholeContextCallable<V> implements Callable<V> {
private final Callable<V> delegate;
// 用戶信息上下文(根據項目實際情況定義ThreadLocal上下文)
private WormholeContext orginWormholeContext;
public DelegatingWormholeContextCallable(Callable<V> delegate,
WormholeContext wormholeContext) {
this.delegate = delegate;
this.orginWormholeContext = wormholeContext;
}
public V call() throws Exception {
//防止線程復用銷毀ThreadLocal的數據
WormholeContextHolder.invalidContext();
// 將當前的用戶上下文設置進Hystrix線程的TreadLocal中
WormholeContextHolder.setContext(orginWormholeContext);
try {
return delegate.call();
} finally {
// 執行完畢,記得清理ThreadLocal資源
WormholeContextHolder.invalidContext();
}
}
public static <V> Callable<V> create(Callable<V> delegate,
WormholeContext wormholeContext) {
return new DelegatingWormholeContextCallable<V>(delegate, wormholeContext);
}
}
實現Hystrix的併發策略類
因為Hystrix預設的併發策略不支持ThreadLocal傳遞,我們可以自定義併發策略類繼承HystrixConcurrencyStrategy
public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy {
// 最簡單的方式就是引入現有的併發策略,進行功能擴展
private final HystrixConcurrencyStrategy existingConcurrencyStrategy;
public ThreadLocalAwareStrategy(
HystrixConcurrencyStrategy existingConcurrencyStrategy) {
this.existingConcurrencyStrategy = existingConcurrencyStrategy;
}
@Override
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.getBlockingQueue(maxQueueSize)
: super.getBlockingQueue(maxQueueSize);
}
@Override
public <T> HystrixRequestVariable<T> getRequestVariable(
HystrixRequestVariableLifecycle<T> rv) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.getRequestVariable(rv)
: super.getRequestVariable(rv);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixProperty<Integer> corePoolSize,
HystrixProperty<Integer> maximumPoolSize,
HystrixProperty<Integer> keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize,
maximumPoolSize, keepAliveTime, unit, workQueue)
: super.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize,
keepAliveTime, unit, workQueue);
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy
.wrapCallable(new DelegatingWormholeContextCallable<>(callable, WormholeContextHolder.getContext()))
: super.wrapCallable(new DelegatingWormholeContextCallable<T>(callable, WormholeContextHolder.getContext()));
}
}
Hystrix註入新併發策略併進行刷新
public class HystrixThreadLocalConfiguration {
@Autowired(required = false)
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
@PostConstruct
public void init() {
// Keeps references of existing Hystrix plugins.
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance()
.getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance()
.getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance()
.getPropertiesStrategy();
HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance()
.getCommandExecutionHook();
HystrixPlugins.reset();
HystrixPlugins.getInstance().registerConcurrencyStrategy(new ThreadLocalAwareStrategy(existingConcurrencyStrategy));
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
}
}
6.2 數據隔離方案
6.2.1 JDBC數據源隔離
數據隔離需要對DB,Redis,RabbitMQ進行數據隔離
通過實現Spring動態數據源AbstractRoutingDataSource
,通過ThreadLocal
識別出來壓測數據,如果是壓測數據就路由到影子庫,如果是正常流量則路由到主庫,通過流量識別的改造,各個服務都已經能夠識別出壓測的請求流量了。
6.2.1.1 代碼實現
數據源路由Key持有對象
根據路由Key將選擇將操作路由給那個數據源
/**
* 動態數據源上下文
*/
public class DynamicDataSourceContextHolder {
public static final String PRIMARY_DB = "primary";
public static final String SHADOW_DB = "shadow";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/**
* 將 master 數據源的 key作為預設數據源的 key
*/
@Override
protected String initialValue() {
return PRIMARY_DB;
}
};
/**
* 數據源的 key集合,用於切換時判斷數據源是否存在
*/
public static List<Object> dataSourceKeys = new ArrayList<>();
/**
* 切換數據源
*
* @param key
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
/**
* 獲取數據源
*
* @return
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* 重置數據源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
/**
* 判斷是否包含數據源
*
* @param key 數據源key
* @return
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
/**
* 添加數據源keys
*
* @param keys
* @return
*/
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}
動態數據源實現類
根據路由Key實現數據源的切換
/**
* 動態數據源實現類
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 如果不希望數據源在啟動配置時就載入好,可以定製這個方法,從任何你希望的地方讀取並返回數據源
* 比如從資料庫、文件、外部介面等讀取數據源信息,並最終返回一個DataSource實現類對象即可
*/
@Override
protected DataSource determineTargetDataSource() {
//獲取當前的上下文
WormholeContext wormholeContext = WormholeContextHolder.getContext();
//如果不為空使用影子庫
if (null != wormholeContext) {
DynamicDataSourceContextHolder.setDataSourceKey(DynamicDataSourceContextHolder.SHADOW_DB);
} else {
//為空則使用主數據源
DynamicDataSourceContextHolder.setDataSourceKey(DynamicDataSourceContextHolder.PRIMARY_DB);
}
return super.determineTargetDataSource();
}
/**
* 如果希望所有數據源在啟動配置時就載入好,這裡通過設置數據源Key值來切換數據,定製這個方法
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
6.2.2 Redis 數據源隔離
同時通過ThreadLocal
識別出來壓測數據,自定義Redis的主鍵的序列化方式,如果是壓測數據則在主鍵後面加上尾碼,這樣就可以通過不同主鍵將Redis數據進行隔離。
6.2.2.1 實現key序列化
public class KeyStringRedisSerializer extends StringRedisSerializer {
@Resource
private WormholeIsolationConfiguration isolationConfiguration;
public byte[] serialize(@Nullable String redisKey) {
WormholeContext wormholeContext = WormholeContextHolder.getContext();
if (null != wormholeContext) {
redisKey = isolationConfiguration.generateIsolationKey(redisKey);
}
return super.serialize(redisKey);
}
}
6.2.2.2 配置序列化器
/**
* Redis 配置類
*/
@Configuration
@ConditionalOnClass({RedisTemplate.class, RedisOperations.class, RedisConnectionFactory.class})
public class WormholeRedisAutoConfiguration {
@Bean
public KeyStringRedisSerializer keyStringRedisSerializer() {
return new KeyStringRedisSerializer();
}
@Bean("redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate();
//使用fastjson序列化
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
// value值的序列化採用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化採用StringRedisSerializer
template.setKeySerializer(keyStringRedisSerializer());
template.setHashKeySerializer(keyStringRedisSerializer());
template.setConnectionFactory(factory);
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setKeySerializer(keyStringRedisSerializer());
template.setHashKeySerializer(keyStringRedisSerializer());
template.setConnectionFactory(factory);
return template;
}
}
6.2.3 RabbitMQ 數據隔離
6.2.3.1 自動創建影子隊列
因為SpringAMQP中的
中的關鍵方法是私有的,無法通過繼承的方式進行實現對以配置好的隊列進行擴展,所以需要自定義該類,來實現對自動創建影子隊列,並和交換器進行綁定
代碼實現
改造RabbitListenerAnnotationBeanPostProcessor
類來實現創建MQ影子隊列以及將影子Key綁定到影子隊列。
public class WormholeRabbitListenerAnnotationBeanPostProcessor extends RabbitListenerAnnotationBeanPostProcessor {
@Resource
private WormholeIsolationConfiguration wormholeIsolationConfiguration;
/**
* routingKey 前置處理器
*
* @param queueName
* @param routingKey
* @return
*/
@Override
public String preProcessingRoutingKey(String queueName, String routingKey) {
//如果是影子隊列就將routingKey轉換為 影子routingKey
if (wormholeIsolationConfiguration.checkIsolation(queueName) && !wormholeIsolationConfiguration.checkIsolation(routingKey)) {
return wormholeIsolationConfiguration.generateIsolationKey(routingKey);
}
return routingKey;
}
/**
* 處理隊列問題,如果來了一個隊列就生成一個shadow的隊列
*
* @param queues
* @return
*/
@Override
public List<String> handelQueues(List<String> queues) {
List<String> isolationQueues = new ArrayList<>();
if (null != queues && !queues.isEmpty()) {
for (String queue : queues) {
//添加shadow隊列
isolationQueues.add(wormholeIsolationConfiguration.generateIsolationKey(queue));
}
queues.addAll(isolationQueues);
}
return queues;
}
}
6.2.3.2 傳遞染色標識
因為MQ是非同步通訊,為了傳遞染色標識,會在發送MQ的時候將染色標識傳遞過來,MQ接收到之後放進當前線程的ThreadLocal
裡面,這個需要擴展Spring的SimpleRabbitListenerContainerFactory
來實現
代碼實現
public class WormholeSimpleRabbitListenerContainerFactory extends SimpleRabbitListenerContainerFactory {
@Override
protected SimpleMessageListenerContainer createContainerInstance() {
SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer();
simpleMessageListenerContainer.setAfterReceivePostProcessors(message -> {
//防止線程復用 銷毀ThreadLocal
WormholeContextHolder.invalidContext();
//獲取消息屬性標識
String wormholeRequestContext = message.getMessageProperties().getHeader(WormholeContextHolder.WORMHOLE_REQUEST_MARK);
if (StringUtils.isNotEmpty(wormholeRequestContext)) {
WormholeContextHolder.setContext(wormholeRequestContext);
}
return message;
});
return simpleMessageListenerContainer;
}
}
6.2.3.3 發送MQ消息處理
同上,需要傳遞染色標識,就通過繼承RabbitTemplate
重寫convertAndSend
方法來實現傳遞染色標識。
public class ShadowRabbitTemplate extends RabbitTemplate {
public ShadowRabbitTemplate(ConnectionFactory connectionFactory) {
super(connectionFactory);
}
@Autowired
private WormholeIsolationConfiguration isolationConfiguration;
@Override
public void send(final String exchange, final String routingKey,
final Message message, @Nullable final CorrelationData correlationData)
throws AmqpException {
WormholeContext wormholeContext = WormholeContextHolder.getContext();
if (null == wormholeContext) {
super.send(exchange, routingKey, message, correlationData);
} else {
message.getMessageProperties().setHeader(WormholeContextHolder.WORMHOLE_REQUEST_MARK, wormholeContext.toString());
//生成Rabbit 隔離Key
String wormholeRoutingKey = isolationConfiguration.generateIsolationKey(routingKey);
//調用父類進行發送
super.send(exchange, wormholeRoutingKey, message, correlationData);
}
}
}
6.3 介面隔離方法
6.3.1 Mock 第三方介面
對於第三方數據介面需要進行隔離,比如簡訊介面,正常的數據需要發送簡訊,對於壓測數據則不能直接調用介面發送簡訊,並且需要能夠識別出來壓測數據,併進行MOCK介面調用。
6.3.1.1 核心類實現
@Aspect
public class WormholeMockSection {
/**
* 切點 攔截@WormholeMock的註解
*/
@Pointcut("@annotation(com.heima.wormhole.component.mock.annotation.WormholeMock)")
public void pointCut() {
}
/**
* 環繞通知
*
* @param point
* @return
* @throws Throwable
*/
@Around("pointCut()")
public Object section(ProceedingJoinPoint point) throws Throwable {
WormholeContext wormholeContext = WormholeContextHolder.getContext();
Object[] parameter = point.getArgs();
//如果沒有wormholeContext 就執行正常方法
if (null == wormholeContext) {
return point.proceed(parameter);
}
//如果存在就執行MOCK方法
WormholeMock wormholeMock = WormholeMockUtils.getMethodAnnotation(point, WormholeMock.class);
if (null != wormholeMock) {
//獲取到 Mock 回調類
WormholeMockCallback wormholeMockCallback = WormholeMockUtils.getWormholeMockCallback(wormholeMock);
if (null != wormholeMockCallback) {
return wormholeMockCallback.handelMockData(parameter);
}
}
return null;
}
}
6.3.1.2 使用方式
在具體方法上面加上註解就可以使用了
@Override
//加入註解進行MOCK測試攔截 設置最大耗時
@WormholeMock(maxDelayTime = 10, minDelayTime = 2)
public boolean send(NotifyVO notifyVO) {
logger.info("開始發送簡訊通知.....");
try {
//模擬發送簡訊耗時
Thread.sleep(5);
} catch (InterruptedException e) {
}
return true;
}
6.4 零侵入方案
如果開發的中間件需要各個微服務大量改造,對開發人員來說就是一個災難,所以這裡採用零侵入的springboot starter 來解決
6.4.1 自動裝配
使用微服務得
@Conditional
來完成配置得自動裝配,這裡用MVC得配置來演示自動裝配,其他得都是類似這樣可以最大限度的優化代碼並提高很高的可擴展性。
/**
* MVC 自動裝配
*/
@Configuration
//當DispatcherServlet存在時該配置類才會被執行到
@ConditionalOnClass(org.springframework.web.servlet.DispatcherServlet.class)
public class WormholeMVCAutoConfiguration {
@ConditionalOnClass
@Bean
public WormholeMVCConfiguration wormholeMVCConfiguration() {
return new WormholeMVCConfiguration();
}
}
6.4.1.1 Conditional 簡介
@Conditional表示僅當所有指定條件都匹配時,組件才有資格註冊 。 該@Conditional註釋可以在以下任一方式使用:
- 作為任何@Bean方法的方法級註釋
- 作為任何類的直接或間接註釋的類型級別註釋 @Component,包括@Configuration類
- 作為元註釋,目的是組成自定義構造型註釋
6.4.1.2 Conditional派生註解
@Conditional派生了很多註解,下麵給個表格列舉一下派生註解的用法
@Conditional派生註解 | 作用(都是判斷是否符合指定的條件) |
---|---|
@ConditionalOnJava | 系統的java版本是否符合要求 |
@ConditionalOnBean | 有指定的Bean類 |
@ConditionalOnMissingBean | 沒有指定的bean類 |
@ConditionalOnExpression | 符合指定的SpEL表達式 |
@ConditionalOnClass | 有指定的類 |
@ConditionalOnMissingClass | 沒有指定的類 |
@ConditionalOnSingleCandidate | 容器只有一個指定的bean,或者這個bean是首選bean |
@ConditionalOnProperty | 指定的property屬性有指定的值 |
@ConditionalOnResource | 路徑下存在指定的資源 |
@ConditionalOnWebApplication | 系統環境是web環境 |
@ConditionalOnNotWebApplication | 系統環境不是web環境 |
@ConditionalOnjndi | JNDI存在指定的項 |
6.4.2 SpringBoot starter
和自動裝配一樣,Spring Boot Starter的目的也是簡化配置,而Spring Boot Starter解決的是依賴管理配置複雜的問題,有了它,當我需要構建一個Web應用程式時,不必再遍歷所有的依賴包,一個一個地添加到項目的依賴管理中,而是只需要一個配置spring-boot-starter-web
。
6.4.2.1 使用規範
在 Spring Boot starter 開發規範中,項目中會有一個空的名為 xxx-spring-boot-starter 的項目,這個項目主要靠 pom.xml 將所有需要的依賴引入進來。同時項目還會有一個 xxx-spring-boot-autoconfigure 項目,這個項目主要寫帶 @Configuration 註解的配置類,在這個類或者類中帶 @Bean 的方法上。
6.4.2.2 項目使用
在 xxx-spring-boot-starter的項目下的resources文件夾下麵新建一個META-INF文件,併在下麵創建spring.factories文件,將我們的自動配置類配置進去
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.wormhole.autoconfiguration.WormholeAutoConfiguration
6.5 服務監控方案
6.5.1 skywalking簡介
Skywalking 是一個APM系統,即應用性能監控系統,為微服務架構和雲原生架構系統設計。它通過探針自動收集所需的指標,併進行分散式追蹤。通過這些調用鏈路以及指標,Skywalking APM會感知應用間關係和服務間關係,併進行相應的指標統計。目前支持鏈路追蹤和監控應用組件如下,基本涵蓋主流框架和容器,如國產PRC Dubbo和motan等,國際化的spring boot,spring cloud都支持了
SkyWalking是分散式系統的應用程式性能監視工具,專為微服務、雲原生架構和基於容器(Docker、K8S、Mesos)架構而設計
SkyWalking是觀察性分析平臺和應用性能管理系統。提供分散式追蹤、服務網格遙測分析、度量聚合和可視化一體化解決方案
6.5.1.1 SkyWalking組件
-
Skywalking Agent: 採集
tracing
(調用鏈數據)和metric
(指標)信息並上報,上報通過HTTP或者gRPC方式發送數據到Skywalking Collector -
Skywalking Collector : 鏈路數據收集器,對agent傳過來的
tracing
和metric
數據進行整合分析通過Analysis Core
模塊處理並落入相關的數據存儲中,同時會通過Query Core
模塊進行二次統計和監控告警 -
Storage: Skywalking的存儲,支持以
ElasticSearch
、Mysql
、TiDB
、H2
等作為存儲介質進行數據存儲 -
UI: Web可視化平臺,用來展示落地的數據,目前官方採納了RocketBot作為SkyWalking的主UI
6.5.2 配置SkyWalking
6.5.2.1 下載SkyWalking
下載SkyWalking的壓縮包,解壓後將壓縮包裡面的agent文件夾放進本地磁碟,探針包含整個目錄,請不要改變目錄結構。
6.5.2.2 Agent配置
通過瞭解配置,可以對一個組件功能有一個大致的瞭解,解壓開skywalking的壓縮包,在agent/config文件夾中可以看到agent的配置文件,從skywalking支持環境變數配置載入,在啟動的時候優先讀取環境變數中的相關配置。
skywalking配置名稱 | 描述 |
---|---|
agent.namespace | 跨進程鏈路中的header,不同的namespace會導致跨進程的鏈路中斷 |
agent.service_name | 一個服務(項目)的唯一標識,這個欄位決定了在sw的UI上的關於service的展示名稱 |
agent.sample_n_per_3_secs | 客戶端採樣率,0或者負數標識禁用,預設-1 |
agent.authentication | 與collector進行通信的安全認證,需要同collector中配置相同 |
agent.ignore_suffix | 忽略特定請求尾碼的trace |
collecttor.backend_service | agent需要同collector進行數據傳輸的IP和埠 |
logging.level | agent記錄日誌級別 |
skywalking agent使用javaagent無侵入式的配合collector實現對分散式系統的追蹤和相關數據的上下文傳遞。
6.5.2.3 配置探針
配置SpringBoot啟動參數,需要填寫如下的運行參數,代碼放在後面,需要的自己粘貼。
-javaagent:D:/data/skywalking/agent/skywalking-agent.jar
-Dskywalking.agent.service_name=storage-server
-Dskywalking.collector.backend_service=172.18.0.50:11800
- javaagent:複製的agent目錄下探針的jar包路徑
- skywalking.agent.service_name:需要在skywalking顯示的服務名稱
- skywalking.collector.backend_service:skywalking服務端地址預設是11800
本文由
傳智教育博學谷
教研團隊發佈。如果本文對您有幫助,歡迎
關註
和點贊
;如果您有任何建議也可留言評論
或私信
,您的支持是我堅持創作的動力。轉載請註明出處!