この記事では、分散アプリケーション下のさまざまなシナリオにおけるグローバル ログ ID の透過的な送信のアイデアを紹介し、ログ クエリによる問題のトラブルシューティングの効率を向上させるために、分散ログ追跡 ID の簡単な実装原理と実際の効果を紹介します。
背景
開発者がシステムの問題を解決するために最も一般的に使用される方法は、システム ログを確認することです。秘書として働いたことのある人も多いと思います。インターフェイスと入出力パラメータを見せてください。ログに異常な情報がないか確認してください。」しかし、同時実行数が多い場合、ログの位置決めを使用するのは依然として面倒であり、他のユーザー/他のスレッドのログも大量に出力され、通過するため、関連するすべてのログをフィルタリングすることは困難です。指定されたリクエスト、ダウンストリームのスレッド/サービスに対応するログ、さらには一部の特別なシナリオのアクセス パラメーターは、GIS 座標やレベル 4 アドレスなどのドキュメント情報を含まない一部のログのみを出力するため、ログを見つけるのが非常に不便になります。
シナリオ分析
私のグループが担当しているシステムは主にWebアプリケーションです 関係するリクエストメソッドは主にspringmvcのサーブレットhttpシナリオ、jsfシナリオ、MQシナリオ、resteasyシナリオ、クローバーシナリオ、easyjobシナリオが含まれます 各シナリオはlogTraceIdを透過的に送信するために異なるメソッドを必要とします次に、上記の各シナリオに対する透過的な伝送ソリューションを 1 つずつ分析します。
その前に、まずログ内の logTraceId の透過的な送信と出力の方法を簡単に理解する必要があります。通常、logTraceId の透過的な送信と出力には MDC を使用します。ただし、MDC の内部使用に基づいて、ThreadLocal は次の場合にのみ有効です。このスレッドとサブスレッド サービスは MDC 内の値が失われるため、ここではコーディング侵入を使用して親子スレッドに関係するすべての場所で値の転送を実現するか、MDCAdapter を上書きします。 Alibaba の TransmittableThreadLocal を使用します。親子スレッド転送問題を解決するには、この記事を参照してください。この問題を解決するには、比較的大まかなコーディング侵入が使用されます。
springmvcのサーブレットのHTTPシナリオ
このシナリオは誰もがよく知っていると思いますが、主なアイデアは、インターセプターを介して logTraceId を透過的に送信し、HandlerInterceptor を実装する新しいクラスを作成することです。
preHandle: ビジネスプロセッサがリクエストを処理する前に呼び出され、logTraceId の設定と透過的な送信が実装されます。
postHandle: ビジネス プロセッサがリクエストを処理した後、ビューを生成する前に実行されます。ここでは実装を空にするだけです。
afterCompletion: DispatcherServlet がリクエストを完全に処理した後に呼び出され、MDC の logTraceId をクリアするために使用されます。
@Slf4j
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
try{
String traceId = MDC.get(LogConstants.MDC_LOG_TRACE_ID_KEY);
if (StringUtils.isBlank(traceId)) {
MDC.put(LogConstants.MDC_LOG_TRACE_ID_KEY, TraceUtils.getTraceId());
}
}catch (RuntimeException e){
log.error("mvc自定义log跟踪拦截器执行异常",e);
}
return true;
}
@Override
public void postHandle(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
try{
MDC.clear();
}catch (RuntimeException ex){
log.error("mvc自定义log跟踪拦截器执行异常",ex);
}
}
}
JSF シーン
jsf は皆さんご存知かと思いますが、jsf ではカスタムフィルターもサポートされており、jsf フィルターの動作モード(以下に示す)に基づいて、グローバルフィルターを設定する(AbstractFilter を継承する)ことで、logTraceId を透過的に送信することができます。はスレッド プールで実行されるため、メッセージ本文の logTraceId を信頼する必要があります。
jsfコンシューマフィルタ:主にコンテキスト環境からlogTraceIdを取得し透過的に送信する実装コードは以下の通り
@Slf4j
public class TraceIdGlobalJsfFilter extends AbstractFilter {
@Override
public ResponseMessage invoke(RequestMessage requestMessage) {
//设置traceId
setAndGetTraceId(requestMessage);
try{
return this.getNext().invoke(requestMessage);
}finally {
}
}
/**
* 设置并返回traceId
* @param requestMessage
* @return
*/
private void setAndGetTraceId(RequestMessage requestMessage) {
try{
String logTraceId = MDC.get(LogConstants.MDC_LOG_TRACE_ID_KEY);
Object logTraceIdObj = requestMessage.getInvocationBody().getAttachment(LogConstants.JSF_LOG_TRACE_ID_KEY);
if(StringUtils.isBlank(logTraceId) && logTraceIdObj == null){
//如果filter和MDC都没有获取到则说明有遗漏,打印日志
if(log.isDebugEnabled()){
log.debug("jsf消费者自定义log跟踪拦截器预警,filter和MDC都没有traceId,jsf信息:{}", JSON.toJSONString(requestMessage));
}
} else if(StringUtils.isBlank(logTraceId) && logTraceIdObj != null) {
//如果MDC没有,filter有,打印日志
if(log.isDebugEnabled()){
log.debug("jsf消费者自定义log跟踪拦截器预警,MDC没有filter有traceId,jsf信息:{}", JSON.toJSONString(requestMessage));
}
} else if(StringUtils.isNotBlank(logTraceId) && logTraceIdObj == null){
//如果MDC有,filter没有,说明是源头已经有了,但是jsf是第一次调,透传
requestMessage.getInvocationBody().addAttachment(LogConstants.JSF_LOG_TRACE_ID_KEY, logTraceId);
}else if(StringUtils.isNotBlank(logTraceId) && logTraceIdObj != null){
//MDC和fitler都有,但是并不相等,则存在问题打印日志
if(log.isDebugEnabled()){
log.debug("jsf消费者自定义log跟踪拦截器预警,MDC和filter都有traceId,jsf信息:{}", JSON.toJSONString(requestMessage));
}
}
}catch (RuntimeException e){
log.error("jsf消费者自定义log跟踪拦截器执行异常",e);
}
}
}
jsfプロバイダフィルタ:コンシューマが透過的に送信するlogTraceIdをメッセージ本文で取得することで実装 実装コードは以下の通り
@Slf4j
public class TraceIdGlobalJsfProducerFilter extends AbstractFilter {
@Override
public ResponseMessage invoke(RequestMessage requestMessage) {
//设置traceId
boolean isNeedClearMdc = transferTraceId(requestMessage);
try{
return this.getNext().invoke(requestMessage);
}finally {
if(isNeedClearMdc){
clear();
}
}
}
/**
* 设置并返回traceId
* @param requestMessage
* @return
*/
private boolean transferTraceId(RequestMessage requestMessage) {
boolean isNeedClearMdc = false;
try{
String logTraceId = MDC.get(LogConstants.MDC_LOG_TRACE_ID_KEY);
Object logTraceIdObj = requestMessage.getInvocationBody().getAttachment(LogConstants.JSF_LOG_TRACE_ID_KEY);
if(StringUtils.isBlank(logTraceId) && logTraceIdObj == null){
//如果filter和MDC都没有获取到,说明存在遗漏场景或是提供给外部系统调用的接口,打印日志进行观察
String traceId = TraceUtils.getTraceId();
MDC.put(LogConstants.MDC_LOG_TRACE_ID_KEY,traceId);
requestMessage.getInvocationBody().addAttachment(LogConstants.JSF_LOG_TRACE_ID_KEY, traceId);
if(log.isDebugEnabled()){
log.debug("jsf生产者自定义log跟踪拦截器预警,filter和MDC都没有traceId,jsf信息:{}", JSON.toJSONString(requestMessage));
}
isNeedClearMdc = true;
} else if(StringUtils.isBlank(logTraceId) && logTraceIdObj != null) {
//如果MDC没有,filter有,说明是被调用方,需要透传下去
MDC.put(LogConstants.MDC_LOG_TRACE_ID_KEY,logTraceIdObj.toString());
isNeedClearMdc = true;
} else if(StringUtils.isNotBlank(logTraceId) && logTraceIdObj == null){
//如果MDC有,filter没有,存在问题,打印日志
if(log.isDebugEnabled()){
log.debug("jsf生产者自定义log跟踪拦截器预警,MDC有filter没有traceId,jsf信息:{}", JSON.toJSONString(requestMessage));
}
isNeedClearMdc = true;
}else if(StringUtils.isNotBlank(logTraceId) && logTraceIdObj != null && !logTraceId.equals(logTraceIdObj.toString())){
//MDC和fitler都有,但是并不相等,则信任filter透传结果
TraceUtils.resetTraceId(logTraceIdObj.toString());
if(log.isDebugEnabled()){
log.debug("jsf生产者自定义log跟踪拦截器预警,MDC和fitler都有traceId,但是并不相等,jsf信息:{}", JSON.toJSONString(requestMessage));
}
}
return isNeedClearMdc;
}catch (RuntimeException e){
log.error("jsf生产者自定义log跟踪拦截器执行异常",e);
return false;
}
}
/**
* 清除MDC
*/
private void clear() {
try{
MDC.clear();
}catch (RuntimeException e){
log.error("jsf生产者自定义log跟踪拦截器执行异常",e);
}
}
}
MQ シナリオ
MQ と言えば皆さんご存知かと思いますが、このシナリオでは主に、プロバイダーがメッセージを送信するときにコンテキスト内の logTraceId を取得し、それを拡張情報の形式でメッセージ本文に設定して透過的に送信し、コンシューマーはそれを実行します。メッセージ本文から取得します
プロデューサー: MessageProducer を継承する新しい抽象クラスを作成し、親クラスの 2 つの送信メソッド (一括送信、単一送信) をオーバーライドします。送信メソッドは主にメッセージ本文の抽象処理メソッド (logTraceId 属性の割り当て) を呼び出し、子ではメッセージ本文をクラス内で処理してから送信する 具体的なコードは以下の通り
@Slf4j
public abstract class BaseTraceIdProducer extends MessageProducer {
private static final String SEPARATOR_COMMA = ",";
public BaseTraceIdProducer() {
}
public BaseTraceIdProducer(TransportManager transportManager) {
super(transportManager);
}
/**
* 获取消息体-单个
* @param messageContext
* @return
*/
protected abstract Message getMessage(MessageContext messageContext);
/** 获取消息体-批量
*
* @param messageContext
* @return
*/
protected abstract List<Message> getMessages(MessageContext messageContext);
/**
* 填充消息体上下文信息
* @param message
* @param messageContext
*/
protected void fillContext(Message message,MessageContext messageContext) {
if(message == null){
return;
}
if(StringUtils.isBlank(messageContext.getLogTraceId())){
String logTraceId = message.getAttribute(LogConstants.JMQ2_LOG_TRACE_ID_KEY);
messageContext.setLogTraceId(logTraceId);
}
if(StringUtils.isBlank(messageContext.getTopic())){
String topic = message.getTopic();
messageContext.setTopic(topic);
}
String businessId = message.getBusinessId();
messageContext.getBusinessIdBuf().append(SEPARATOR_COMMA).append(businessId);
}
/**
* traceId嵌入消息体中
* @param message
*/
protected void generateTraceIdIntoMessage(Message message){
if(message == null){
return;
}
try{
String logTraceId = MDC.get(LogConstants.MDC_LOG_TRACE_ID_KEY);
if(StringUtils.isBlank(logTraceId)){
logTraceId = TraceUtils.getTraceId();
MDC.put(LogConstants.MDC_LOG_TRACE_ID_KEY,logTraceId);
}
message.setAttribute(LogConstants.JMQ2_LOG_TRACE_ID_KEY,logTraceId);
}catch (RuntimeException e){
log.error("jmq2自定义log跟踪拦截器执行异常",e);
}
}
/**
* 批量发送消息-无回调
* @param messages
* @param timeout
* @throws JMQException
*/
public void send(List<Message> messages, int timeout) throws JMQException {
MessageContext messageContext = new MessageContext();
messageContext.setMessages(messages);
List<Message> messageList = this.getMessages(messageContext);
//打印日志,方便排查问题
printLog(messageContext);
super.send(messageList, timeout);
}
/**
* 单个发送消息
* @param message
* @param transaction
* @param <T>
* @return
* @throws JMQException
*/
public <T> T send(Message message, LocalTransaction<T> transaction) throws JMQException {
MessageContext messageContext = new MessageContext();
messageContext.setMessage(message);
Message msg = this.getMessage(messageContext);
//打印日志,方便排查问题
printLog(messageContext);
return super.send(msg, transaction);
}
/**
* 批量发送消息-有回调
* @param messages
* @param timeout
* @param callback
* @throws JMQException
*/
public void send(List<Message> messages, int timeout, AsyncSendCallback callback) throws JMQException {
MessageContext messageContext = new MessageContext();
messageContext.setMessages(messages);
List<Message> messageList = this.getMessages(messageContext);
//打印日志,方便排查问题
printLog(messageContext);
super.send(messageList, timeout, callback);
}
/**
* 打印日志,方便排查问题
* @param messageContext
*/
private void printLog(MessageContext messageContext) {
if(messageContext==null){
return;
}
if(log.isInfoEnabled()){
log.info("MQ发送:traceId:{},topic:{},businessIds:[{}]",messageContext.getLogTraceId(),messageContext.getTopic(),messageContext.getBusinessIdBuf()==null?"":messageContext.getBusinessIdBuf().toString());
}
}
}
@Slf4j
public class TraceIdEnvMessageProducer extends BaseTraceIdProducer {
private static final String UAT_TRUE = String.valueOf(true);
private boolean uat = false;
public TraceIdEnvMessageProducer() {
}
public TraceIdEnvMessageProducer(TransportManager transportManager) {
super(transportManager);
}
/**
* 环境变量打标-单个消息体
* @param message
*/
private void convertUatMessage(Message message) {
if (message != null) {
message.setAttribute(SplitMessage.JMQ_SPLIT_KEY_IS_UAT, UAT_TRUE);
}
}
/**
* 消息转换-批量消息体
* @param messageContext
* @return
*/
private List<Message> convertMessages(MessageContext messageContext) {
List<Message> messages = messageContext.getMessages();
if (!CollectionUtils.isEmpty(messages)) {
Iterator messageIterator = messages.iterator();
while(messageIterator.hasNext()) {
Message message = (Message)messageIterator.next();
if(this.isUat()){
this.convertUatMessage(message);
}
super.generateTraceIdIntoMessage(message);
super.fillContext(message,messageContext);
}
}
return messageContext.getMessages();
}
/**
* 消息转换-单个消息体
* @param messageContext
* @return
*/
private Message convertMessage(MessageContext messageContext){
Message message = messageContext.getMessage();
if(this.isUat()){
this.convertUatMessage(message);
}
super.generateTraceIdIntoMessage(message);
super.fillContext(message,messageContext);
return message;
}
protected Message getMessage(MessageContext messageContext) {
if(log.isDebugEnabled()){
log.debug("current environment is UAT : {}", this.isUat());
}
return this.convertMessage(messageContext);
}
protected List<Message> getMessages(MessageContext messageContext) {
if(log.isDebugEnabled()){
log.debug("current environment is UAT : {}", this.isUat());
}
return this.convertMessages(messageContext);
}
public void setUat(boolean uat) {
this.uat = uat;
}
boolean isUat() {
return this.uat;
}
}
Consumer: MessageListenerを継承する新しい抽象クラスを作成し、親クラスのonMessageメソッドをオーバーライドし、主にログのtraceIdを設定し、消費完了後にtraceIdを消去するなど、サブクラスでいくつかのカスタム処理を実行します。具体的なコードは次のとおりです。次のように
@Slf4j
public abstract class BaseTraceIdMessageListener implements MessageListener {
public BaseTraceIdMessageListener() {
}
public abstract void onMessageList(List<Message> messages) throws Exception;
@Override
public final void onMessage(List<Message> messages) throws Exception {
try{
if(CollectionUtils.isEmpty(messages)){
return;
}
//设置日志traceId
setLogTraceId(messages);
this.onMessageList(messages);
//消费完后清除traceId
clear();
}catch (Exception e){
throw e;
}finally {
MDC.clear();
}
}
/**
* 设置日志traceId
* @param messages
*/
private void setLogTraceId(List<Message> messages) {
try{
Message message = messages.get(0);
String logTraceId = message.getAttribute(LogConstants.JMQ2_LOG_TRACE_ID_KEY);
if(StringUtils.isBlank(logTraceId)){
logTraceId = TraceUtils.getTraceId();
}
MDC.put(LogConstants.MDC_LOG_TRACE_ID_KEY,logTraceId);
}catch (RuntimeException e){
log.error("jmq2自定义log跟踪拦截器执行异常",e);
}
}
/**
* 清除traceId
*/
private void clear() {
try{
MDC.clear();
}catch (RuntimeException e){
log.error("jmq2自定义log跟踪拦截器执行异常",e);
}
}
}
@Slf4j
public abstract class TraceIdEnvMessageListener extends BaseTraceIdMessageListener{
private String uat;
public TraceIdEnvMessageListener() {
}
public abstract void onMessages(List<Message> var1) throws Exception;
@Override
public void onMessageList(List<Message> messages) throws Exception {
Iterator iterator;
Message message;
if (this.getUat() != null && Boolean.valueOf(this.getUat())) {
iterator = messages.iterator();
while(true) {
while(iterator.hasNext()) {
message = (Message)iterator.next();
if (message != null && Boolean.valueOf(message.getAttribute(SplitMessage.JMQ_SPLIT_KEY_IS_UAT))) {
this.onMessages(Arrays.asList(message));
} else {
log.debug("Ignore message: [BusinessId: {}, Text: {}]", message.getBusinessId(), message.getText());
}
}
return;
}
} else if (this.getUat() != null && !Boolean.valueOf(this.getUat())) {
iterator = messages.iterator();
while(true) {
while(iterator.hasNext()) {
message = (Message)iterator.next();
if (message != null && !Boolean.valueOf(message.getAttribute(SplitMessage.JMQ_SPLIT_KEY_IS_UAT))) {
this.onMessages(Arrays.asList(message));
} else {
log.debug("Ignore message: [BusinessId: {}, Text: {}]", message.getBusinessId(), message.getText());
}
}
return;
}
} else {
this.onMessages(messages);
}
}
public void setUat(String uat) {
if (!"true".equals(uat) && !"false".equals(uat)) {
throw new IllegalArgumentException("uat 属性值只能为 true 或 false.");
} else {
this.uat = uat;
}
}
public String getUat() {
return this.uat;
}
}
くつろぎのシーン
このシナリオは、spinrg-mvc シナリオに似ています。これは http リクエストでもあります。logTraceId は、インターセプタを介してメッセージ ヘッダーで透過的に送信される必要があります。主にクライアント インターセプタとサーバー側があります: 前処理インターセプタとポスト インターセプタ。コード以下のとおりであります
@ClientInterceptor
@Provider
@Slf4j
public class ResteasyClientInterceptor implements ClientExecutionInterceptor {
@Override
public ClientResponse execute(ClientExecutionContext clientExecutionContext) throws Exception {
try{
String logTraceId = MDC.get(LogConstants.MDC_LOG_TRACE_ID_KEY);
ClientRequest request = clientExecutionContext.getRequest();
String headerTraceId = request.getHeaders().getFirst(LogConstants.HEADER_LOG_TRACE_ID_KEY);
if(StringUtils.isBlank(logTraceId) && StringUtils.isBlank(headerTraceId)){
//如果filter和MDC都没有获取到则说明是调用源头
String traceId = TraceUtils.getTraceId();
TraceUtils.resetTraceId(traceId);
request.header(LogConstants.HEADER_LOG_TRACE_ID_KEY,traceId);
} else if(StringUtils.isBlank(headerTraceId)){
//如果MDC有但是filter没有则需要传递
request.header(LogConstants.HEADER_LOG_TRACE_ID_KEY,logTraceId);
}
}catch (RuntimeException e){
log.error("resteasy客户端log跟踪拦截器执行异常",e);
}
return clientExecutionContext.proceed();
}
}
@Slf4j
@Provider
@ServerInterceptor
public class RestEasyPreInterceptor implements PreProcessInterceptor {
@Override
public ServerResponse preProcess(HttpRequest request, ResourceMethod resourceMethod) throws Failure, WebApplicationException {
try{
MultivaluedMap<String, String> requestHeaders = request.getHttpHeaders().getRequestHeaders();
String headerTraceId = requestHeaders.getFirst(LogConstants.HEADER_LOG_TRACE_ID_KEY);
if(StringUtils.isNotBlank(headerTraceId)){
//如果filter则透传
TraceUtils.resetTraceId(headerTraceId);
}
}catch (RuntimeException e){
log.error("resteasy服务端log跟踪前置拦截器执行异常",e);
}
return null;
}
}
@Slf4j
@Provider
@ServerInterceptor
public class ResteasyPostInterceptor implements PostProcessInterceptor {
@Override
public void postProcess(ServerResponse serverResponse) {
try{
MDC.clear();
}catch (RuntimeException e){
log.error("resteasy服务端log跟踪后置拦截器执行异常",e);
}
}
}
クローバーのシーン
クローバーの一般的なメカニズムは、プロジェクトの開始時にアノテーション @HessianWebService を持つクラスをスキャンしてサービスを登録し、ハートビート検出を維持することです。クローバー側はサーブレット リクエストを通じてタスク コールバックを実行します。同時に、AbstractScheduleTaskProcess メソッドを継承するタスクがスレッド化されます。 . プール形式での業務処理
1. ServiceExporterServlet を継承する新しいクラスを作成し、web.xml 構成でサーブレットを構成します。コードは次のとおりです。
@Slf4j
public class ServiceExporterTraceIdServlet extends ServiceExporterServlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
try {
String traceId = MDC.get("traceId");
if (StringUtils.isBlank(traceId)) {
MDC.put("traceId", TraceUtils.getTraceId());
}
} catch (Exception e) {
log.error("clover请求servlet执行异常", e);
}
try {
super.service(req, res);
} catch (Throwable e) {
log.error("clover请求servlet执行异常", e);
throw e;
}finally {
try{
MDC.clear();
}catch (RuntimeException ex){
log.error("clover请求servlet执行异常",ex);
}
}
}
}
2. AbstractScheduleTaskProcessを継承した新規抽象クラスを作成し、クラス内のコーディング形式で親子スレッドの透過的な送信を行う(最適化可能:MDCAdapterの上書きにより:AlibabaのTransmittableThreadLocalにより親子スレッド送信の問題を解決) 、すべてのタスクは代わりにこのクラスを継承します。キーコードは次のとおりです。
try{
traceId = MDC.get(LogConstants.MDC_LOG_TRACE_ID_KEY);
if (StringUtils.isBlank(traceId)) {
log.warn("clover自定义log跟踪拦截器预警,mdc没有traceId");
}
}catch (RuntimeException e){
log.error("clover自定义log跟踪拦截器执行异常",e);
}
final String logTraceId = traceId;
while(iterator.hasNext()) {
final List<TcTask> list = (List<TcTask>)iterator.next();
this.executor.submit(new Callable<Object>() {
public Object call() throws Exception {
try{
if (StringUtils.isNotBlank(logTraceId)) {
MDC.put(LogConstants.MDC_LOG_TRACE_ID_KEY, logTraceId);
}
}catch (RuntimeException e){
log.error("clover自定义log跟踪拦截器执行异常",e);
}
Object var1;
try {
if (BaseTcTaskProcessWorker.logger.isInfoEnabled()) {
BaseTcTaskProcessWorker.logger.info("正在执行任务[" + this.getClass().getName() + "],条数:" + list.size() + "...");
}
BaseTcTaskProcessWorker.this.executeTasks(list);
if (BaseTcTaskProcessWorker.logger.isInfoEnabled()) {
BaseTcTaskProcessWorker.logger.info("执行任务[" + this.getClass().getName() + "],条数:" + list.size() + "成功!");
}
var1 = null;
} catch (Exception var5) {
BaseTcTaskProcessWorker.logger.error(var5.getMessage(), var5);
throw var5;
} finally {
try{
MDC.clear();
}catch (RuntimeException ex){
log.error("clover自定义log跟踪拦截器执行异常",ex);
}
latch.countDown();
}
return var1;
}
});
}
簡単な仕事のシーン
easyjobの一般的な仕組みは、プロジェクトの開始時にレポートと登録のためにインターフェースSchedulerを実装したクラスをスキャンし、同時にアクセプタ(タスクを取得するスレッドプール)を起動し、アクセプタがタスクをプルした後、スレッド プール、slowExecutor と呼ばれるスレッド プールにサブタスクを入れ、新しい宝くじクラス実装インターフェイス ScheduleFlowTask を作成し、クローバー シーンのハード コーディング メソッドを再利用して親の logTraceId を透過的に送信し、子スレッド (最適化可能: MDCAdapter を上書き: Alibaba TransmittableThreadLocal を通じて親子スレッド転送の問題を解決)、サンプルコードは次のとおりです。
@Slf4j
public abstract class AbstractEasyjobOnlyScheduleProcess<T> implements ScheduleFlowTask {
/**
* EASYJOB平台UMP监控key前缀
*/
private static final String EASYJOB_UMP_KEY_RREFIX = "trans.easyjob.dotask.";
/**
* EASYJOB单个任务处理分布式锁前缀
*/
private static final String EASYJOB_SINGLE_TASK_LOCK_PREFIX = "basic_easyjob_single_task_lock_prefix_";
/**
* 环境标识-开关配置进行环境隔离
*/
@Value("${spring.profiles.active}")
private String activeEnv;
@Value("${task.scene.mark}")
private String sceneMark = TaskSceneMarkEnum.PRODUCTION.getDesc();
/**
* easyJob维度线程池变量
*/
private ThreadPoolExecutor easyJobExecutor;
/**
* easyJob维度服务器个数-分片个数
*/
private volatile int easyJobLastThreadCount = 0;
/**
* easyjob多线程名称
*/
private static final String EASYJOB_THREAD_NAME = "dts.easyJobs";
/**
* 子类的泛型参数类型
*/
private Class<T> argumentType;
/**
* 无参构造
*/
public AbstractEasyjobOnlyScheduleProcess() {
//设置子类泛型参数类型
argumentType = this.getArgumentType();
}
@Autowired
private RedisHelper redisHelper;
/**
* 非task表扫描待处理的任务数据
* @param taskServerParam
* @param curServer
* @return
*/
protected abstract List<T> loadTasks(TaskServerParam taskServerParam, int curServer);
/**
* 业务处理抽象方法-单个
* @param task
*/
protected abstract void doSingleTask(T task);
/**
* 业务处理抽象方法-批量
* @param tasks
*/
protected abstract void doBatchTasks(List<T> tasks);
/**
* 拼装ump监控key
* @param prefix
* @param taskNameKey
* @return
*/
private String getUmpKey(String prefix,String taskNameKey) {
StringBuffer umpKeyBuf = new StringBuffer();
umpKeyBuf.append(prefix).append(taskNameKey);
return umpKeyBuf.toString();
}
/**
* easyjob平台异步任务回调方法
* @param scheduleContext
* @return
* @throws Exception
*/
@Override
public TaskResult doTask(ScheduleContext scheduleContext) throws Exception {
String requestNo = TraceUtils.getTraceId();
try {
String traceId = MDC.get(LogConstants.MDC_LOG_TRACE_ID_KEY);
if (StringUtils.isBlank(traceId)) {
MDC.put(LogConstants.MDC_LOG_TRACE_ID_KEY, requestNo);
}
} catch (Exception e) {
log.error("easyjob执行异常", e);
}
EasyJobTaskServerParam taskServerParam = null;
CallerInfo callerinfo = null;
try {
//条件转换
taskServerParam = EasyJobCoreUtil.transTaskServerParam(scheduleContext);
String taskNameKey = getTaskNameKey();
String umpKey = getUmpKey(EASYJOB_UMP_KEY_RREFIX,taskNameKey);
callerinfo = Profiler.registerInfo(umpKey, Constants.TRANS_BASIC, false, true);
//多服务器,并且非子任务,本次不执行,提交子任务
if (taskServerParam.getServerCount() > 1 && !taskServerParam.isSubTask()) {
submitSubTask(scheduleContext, taskServerParam,requestNo);
return TaskResult.success();
}
if (log.isInfoEnabled()) {
log.info("请求编号[{}],开始获取任务,任务ID[{}],任务名称[{}],执行参数[{}]", requestNo, taskServerParam.getTaskId(), taskServerParam.getTaskName(), JSON.toJSONString(taskServerParam));
}
TaskServerParam cloverTaskServerParam = EasyJobCoreUtil.transferCloverTaskServerParam(taskServerParam);
List<T> tasks = this.selectTasks(cloverTaskServerParam, taskServerParam.getCurServer());
if (log.isInfoEnabled()) {
log.info("请求编号[{}],获取任务ID[{}],任务名称[{}]共{}条", requestNo, taskServerParam.getTaskId(), taskServerParam.getTaskName(), tasks == null ? 0 : tasks.size());
}
if (CollectionUtils.isNotEmpty(tasks)) {
if (log.isInfoEnabled()) {
log.info("请求编号[{}],开始执行任务,任务ID[{}],任务名称[{}]", requestNo, taskServerParam.getTaskId(), taskServerParam.getTaskName());
}
this.easyJobExecuteTasksInner(taskServerParam, tasks,requestNo);
if (log.isInfoEnabled()) {
log.info("请求编号[{}],执行任务,任务ID[{}],任务名称[{}],执行数量[{}]完成....", requestNo, taskServerParam.getTaskId(), taskServerParam.getTaskName(), tasks.size());
}
}
return TaskResult.success();
} catch (Exception e) {
Profiler.functionError(callerinfo);
if (log.isInfoEnabled()) {
log.error("请求编号[{}],任务执行失败,任务ID[{}],任务名称[{}]", requestNo, taskServerParam == null ? "" : taskServerParam.getTaskId(), taskServerParam == null ? "" :taskServerParam.getTaskName(), e);
}
return TaskResult.fail(e.getMessage());
}finally {
try{
MDC.clear();
}catch (RuntimeException ex){
log.error("easyjob执行异常",ex);
}
Profiler.registerInfoEnd(callerinfo);
}
}
/**
* 多分片提交子任务
* @param scheduleContext 调度任务上下文参数
* @param taskServerParam 调度任务参数
* @param requestNo 调度任务参数
* @return void
*/
private void submitSubTask(ScheduleContext scheduleContext, EasyJobTaskServerParam taskServerParam,String requestNo) throws IOException {
log.info("请求编号[{}],执行任务,任务ID[{}],任务名称[{}],子任务个数[{}],开始提交子任务", requestNo, taskServerParam.getTaskId(), taskServerParam.getTaskName(), taskServerParam.getServerCount());
String jobClass = scheduleContext.getTaskGetResponse().getJobClass();
if (StringUtils.isBlank(jobClass)) {
throw new RuntimeException("jobClass get error");
}
for (int i = 0; i < taskServerParam.getServerCount(); i++) {
Map<String, String> dataMap = scheduleContext.getParameters();
//提交子任务标识
dataMap.put("isSubTask", "true");
//给子任务进行编号
dataMap.put("curServer", String.valueOf(i));
//父任务名称传递子任务
dataMap.put("taskName", taskServerParam.getTaskName());
scheduleContext.commitSubTask(jobClass, dataMap, taskServerParam.getExpected(), taskServerParam.getTransactionalAccept());
}
// 父任务等待子任务执行完毕再更改状态,如果执行时间超过等待时间,抛异常
//scheduleContext.waitForSubtaskCompleted((long) taskServerParam.getServerCount() * taskServerParam.getExpected());
log.info("请求编号[{}],执行任务,任务ID[{}],任务名称[{}],子任务个数[{}],提交完成....", requestNo, taskServerParam.getTaskId(), taskServerParam.getTaskName(), taskServerParam.getServerCount());
}
/**
* 创建线程池,按配置参数执行task
* @param param 执行参数
* @param tasks 任务集合
* @param requestNoStr
* @return void
*/
private void easyJobExecuteTasksInner(final EasyJobTaskServerParam param, List<T> tasks,String requestNoStr) {
int threadCount = param.getThreadCount();
synchronized (this) {
if (this.easyJobExecutor == null) {
this.easyJobExecutor = (ThreadPoolExecutor) EasyJobCoreUtil.createCustomeasyJobExecutorService(threadCount, EASYJOB_THREAD_NAME);
this.easyJobLastThreadCount = threadCount;
} else if (threadCount > this.easyJobLastThreadCount) {
this.easyJobExecutor.setMaximumPoolSize(threadCount);
this.easyJobExecutor.setCorePoolSize(threadCount);
this.easyJobLastThreadCount = threadCount;
} else if (threadCount < this.easyJobLastThreadCount) {
this.easyJobExecutor.setCorePoolSize(threadCount);
this.easyJobExecutor.setMaximumPoolSize(threadCount);
this.easyJobLastThreadCount = threadCount;
}
}
List<List<T>> lists = Lists.partition(tasks, param.getExecuteCount());
final CountDownLatch latch = new CountDownLatch(lists.size());
final String requestNo = requestNoStr;
for (final List<T> list : lists) {
this.easyJobExecutor.submit(
new Callable<Object>() {
public Object call() throws Exception {
try{
if (StringUtils.isNotBlank(requestNo)) {
MDC.put(LogConstants.MDC_LOG_TRACE_ID_KEY, requestNo);
}
}catch (RuntimeException e){
log.error("easyjob自定义log跟踪拦截器执行异常",e);
}
try {
if (log.isInfoEnabled()) {
log.info("请求编号[{}],正在执行任务,任务ID[{}],任务名称[{}],[{}],条数:[{}]...", requestNo, param.getTaskId(), param.getTaskName(), Thread.currentThread().getName(), list.size());
}
executeTasks(list);
if (log.isInfoEnabled()) {
log.info("请求编号[{}],执行任务,任务ID[{}],任务名称[{}],[{}],条数:[{}]成功!", requestNo, param.getTaskId(), param.getTaskName(), Thread.currentThread().getName(), list.size());
}
} catch (Exception e) {
log.error(e.getMessage(), e);
throw e;
} finally {
try{
MDC.clear();
}catch (RuntimeException ex){
log.error("easyjob自定义log跟踪拦截器执行异常",ex);
}
latch.countDown();
}
return null;
}
}
);
}
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException("interrupted when processing data access request in concurrency", e);
}
}
/**
* 获取任务名称
* @return
*/
private String getTaskNameKey(){
StringBuffer keyBuf = new StringBuffer();
keyBuf.append(activeEnv)
.append(Constants.SEPARATOR_UNDERLINE)
.append(this.getClass().getSimpleName());
return keyBuf.toString();
}
protected void executeTasks(List<T> taskList) {
if(CollectionUtils.isEmpty(taskList)) {
return;
}
this.doTasks(taskList);
}
/**
* 业务处理抽象方法
* @param list
*/
protected void doTasks(List<T> list){
if(isDoBatchTasks()){
CallerInfo info = Profiler.registerInfo(getClass().getName()+"_batch", Constants.TRANS_BASIC,false, true);
try {
/** 开始执行各个子类真正业务逻辑 */
this.doBatchTasks(list);
} catch(CommonBusinessException ex){
log.warn(ex.getMessage());
} catch (Exception e) {
Profiler.functionError(info);
log.error("任务处理失败,方法:{},任务:{}",ClassHelper.getMethod(),JSON.toJSONString(list), e);
} finally {
Profiler.registerInfoEnd(info);
}
}else{
for (T task : list) {
CallerInfo info = Profiler.registerInfo(getClass().getName(), Constants.TRANS_BASIC,false, true);
if(task == null) { continue; }
String lockKey = "";
try {
/** 开始执行各个子类真正业务逻辑 */
if (useConcurrentLock()) {
lockKey = getLockKey(task);
if (redisHelper.lock(RedisKeyDef.SyncLockKeyPrefix.TASK_PROCESS_LOCK_PREFIX, lockKey)) {
this.doSingleTask(task);
}else{
lockKey = "";
log.warn("lockKey:{},加载失败,正在被其他用户锁定,请重试!",lockKey);
}
} else {
this.doSingleTask(task);
}
} catch(CommonBusinessException ex){
log.warn(ex.getMessage());
} catch (Exception e) {
Profiler.functionError(info);
log.error("任务处理失败,方法:{},任务:{}",ClassHelper.getMethod(),JSON.toJSONString(task), e);
} finally {
Profiler.registerInfoEnd(info);
if (StringUtils.isNotBlank(lockKey)) {
redisHelper.unlock(RedisKeyDef.SyncLockKeyPrefix.TASK_PROCESS_LOCK_PREFIX, lockKey);
}
}
}
}
}
/**
* 获取实体类的实际类型
*
* @return
*/
private Class<T> getArgumentType() {
return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
/**
* 是否使用防并发锁
* 默认不使用,如需使用子类重写该方法
* @return
*/
protected boolean useConcurrentLock() {
return false;
}
/**
* 根所注解获取LockKey,可被子类重写,提高效率
*
* @param businessObj 业务对象
* @return concurrent lock key
*/
protected String getLockKey( T businessObj) {
StringBuilder lockKey = new StringBuilder(EASYJOB_SINGLE_TASK_LOCK_PREFIX);
//若存在注解指定的防重字段,则使用这些字段拼装防重Key,否则使用MQ业务主键防重
List<ValueEntryInfo> valueEntries = getAnnotaionConcurrentKeys(businessObj);
if (!CollectionUtils.isEmpty(valueEntries)) {
for (ValueEntryInfo valueEntry : valueEntries) {
lockKey.append(Constants.SEPARATOR_UNDERLINE);
lockKey.append(valueEntry.getValue());
}
} else {
throw new CommonBusinessException(String.format("此任务处理需要加分布式锁,但是未设置锁key,所以不做业务处理,请检查,任务信息:%s",JSON.toJSONString(businessObj)));
}
return lockKey.toString();
}
/**
* 查找对象的ConccurentKey注解,获取防重字段,并排序返回
*
* @param businessObj 业务对象
* @return 有序的业务字段值列表
*/
private List<ValueEntryInfo> getAnnotaionConcurrentKeys(T businessObj) {
List<ValueEntryInfo> valueEntries = new ArrayList<ValueEntryInfo>();
Field[] fields = businessObj.getClass().getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
ConcurrentKey concurrentKey = fields[i].getAnnotation(ConcurrentKey.class);
if (concurrentKey != null) {
fields[i].setAccessible(true);
Object fieldVal = null;
try {
ValueEntryInfo valueEntry = new ValueEntryInfo();
fieldVal = fields[i].get(businessObj);
if (fieldVal != null) {
valueEntry.setValue(String.format("%1$s", fieldVal));
valueEntry.setOrder(concurrentKey.order());
valueEntries.add(valueEntry);
}
} catch (IllegalAccessException e) {
log.error("IllegalAccess-{}.{}", businessObj.getClass().getName(), fields[i].getName());
}
}
}
if (valueEntries.size() > 1) {
//排序ConcurrentKey
Collections.sort(valueEntries, new Comparator<ValueEntryInfo>() {
@Override
public int compare(ValueEntryInfo o1, ValueEntryInfo o2) {
if (o1.getOrder() > o2.getOrder()) {
return 1;
} else if (o1.getOrder() == o2.getOrder()) {
return 0;
} else {
return -1;
}
}
});
}
return valueEntries;
}
protected List<T> selectTasks(TaskServerParam taskServerParam, int curServer) {
return this.loadTasks(taskServerParam, curServer);
}
/**
* 获取select时的任务创建开始时间
* @param serverArg
* @return
*/
protected Date getCreateTimeFrom(String serverArg){
return null;
}
/**
* 是否以批量方式处理任务
* @return
*/
protected boolean isDoBatchTasks(){
return false;
}
}
実績
以上が透過ID送信シナリオの原理とサンプルコードですが、実際の効果としては、jsfの呼び出しがタイムアウトになり、トラブルシューティングのためにシステム全体のログを確認したところ、SQLの遅さが原因であることが分かりました。
上記のシナリオのほとんどで共通の jar パッケージが抽出されています。詳しい使用方法のチュートリアルについては、私の別の記事「分散ログ トラッキング ID の使用方法のチュートリアル」を参照してください。
著者: JD Logistics Zhang Xiaolong
出典:JD Cloud Developer Community Ziyuanqishuo Tech 転載の際は出典を明記してください
Bilibiliは2度クラッシュ、テンセントの「3.29」第1レベル事故…2023年のダウンタイム事故トップ10を振り返る Vue 3.4「スラムダンク」リリース MySQL 5.7、莫曲、李条条…2023年の「停止」を振り返る 続き” (オープンソース) プロジェクトと Web サイトが 30 年前の IDE を振り返る: TUI のみ、明るい背景色... Vim 9.1 がリリース、 Redis の父 Bram Moolenaar に捧げ、「ラピッド レビュー」LLM プログラミング: Omniscient 全能&&愚かな 「ポスト・オープンソースの時代が来た。ライセンスの有効期限が切れ、一般ユーザーにサービスを提供できなくなった。チャイナ ユニコムブロードバンドが突然アップロード速度を制限し、多くのユーザーが苦情を申し立てた。Windows 幹部は改善を約束した: Make the Start」メニューもまた素晴らしいです。 パスカルの父、ニクラス・ヴィルトが亡くなりました。