房價網是怎麼使用分散式作業框架elastic-job

猿天地發表於2019-03-04

Elastic-Job是什麼?

Elastic-Job是一個分散式排程解決方案,由兩個相互獨立的子專案Elastic-Job-Lite和Elastic-Job-Cloud組成。

Elastic-Job-Lite定位為輕量級無中心化解決方案,使用jar包的形式提供分散式任務的協調服務;Elastic-Job-Cloud採用自研Mesos Framework的解決方案,額外提供資源治理、應用分發以及程式隔離等功能。

官網地址:elasticjob.io/

Github:github.com/elasticjob/…

為什麼要使用Elastic-Job

目前我們公司用的是基於Linux Crontab的定時任務執行器。

存在如下問題:

  • 無法集中管理任務
  • 不能水平擴充套件
  • 無視覺化介面操作
  • 存在單點故障

除了Linux Crontab在java這塊的方案還有 Quartz,但 Quartz缺少分散式並行排程的功能。

存在的問題也很明顯:

  • 當我的專案是一個單體應用時,在裡面基於Quartz起一個定時任務,可以很愉快的執行
  • 當我的專案做了負載,擴充到3臺節點時,3個節點上的任務會同時執行,資料亂了
  • 同時執行要保證資料沒問題需要引入分散式鎖來排程,難度增大

怎麼解決?

1.自研框架

這種情況下可能需要自己去開發一個能夠滿足公司業務需求的排程框架,成本較高,不推薦

之前我也有想過要自己寫一個,思路有了,就是還沒開始,排程框架只要是排程問題,像Elastic-Job就做的非常好,它把分片的規則讓你自己定義,然後根據你定的片的資料給你排程下,至於每個節點處理什麼資料你自己去控制。

如果說部採用這種方式,也不去寫資料的分發,那麼我覺得最簡單的辦法就是用訊息佇列來實現了。

採用zookeeper來做排程,儲存任務資料,定義一個通用的介面,分成2部分,如下:

public interface Job {
    void read();
    void process(Object data);
}
複製程式碼

然後使用者通過實現上面的介面來讀取需要處理的資料,在process中處理分發過來的資料

至於分發的話一個任務可以通過註解來標記使用一個佇列,也可以使用通用的,這樣就可以實現多個消費者同時消費了,就算其中一個掛掉也不影響整個任務,也不用考慮失效轉移了。

但是要做控制的是read方法,必須只有一個節點執行,不然資料就分發重複了。

上面只是提供一個簡單的思路,當然有web頁面管理任務,也可以手動執行任務等等。

2.選擇開源方案

TBSchedule:阿里早期開源的分散式任務排程系統。程式碼略陳舊,使用timer而非執行緒池執行任務排程。眾所周知,timer在處理異常狀況時是有缺陷的。而且TBSchedule作業型別較為單一,只能是獲取/處理資料一種模式。還有就是文件缺失比較嚴重。

Spring Batch: Spring Batch是一個輕量級的,完全面向Spring的批處理框架,可以應用於企業級大量的資料處理系統。Spring Batch以POJO和大家熟知的Spring框架為基礎,使開發者更容易的訪問和利用企業級服務。Spring Batch可以提供大量的,可重複的資料處理功能,包括日誌記錄/跟蹤,事務管理,作業處理統計工作重新啟動、跳過,和資源管理等重要功能。

Elastic-Job:國內開源產品,中文文件,入門快速,使用簡單,功能齊全,社群活躍,由噹噹網架構師張亮主導,目前在開源方面投入了比較多的時間。

為什麼選擇Elastic-Job?

  • 分散式排程協調
  • 彈性擴容縮容
  • 失效轉移
  • 錯過執行作業重觸發
  • 作業分片一致性,保證同一分片在分散式環境中僅一個執行例項
  • 自診斷並修復分散式不穩定造成的問題
  • 支援並行排程
  • 支援作業生命週期操作
  • 豐富的作業型別
  • Spring整合以及名稱空間提供
  • 運維平臺

作業型別介紹

Simple:簡單作業,常用, 意為簡單實現,未經任何封裝的型別。需實現SimpleJob介面。該介面僅提供單一方法用於覆蓋,此方法將定時執行。與Quartz原生介面相似,但提供了彈性擴縮容和分片等功能。

public class MyElasticJob implements SimpleJob {
    
    @Override
    public void execute(ShardingContext context) {
        switch (context.getShardingItem()) {
            case 0: 
                // do something by sharding item 0
                break;
            case 1: 
                // do something by sharding item 1
                break;
            case 2: 
                // do something by sharding item 2
                break;
            // case n: ...
        }
    }
}
複製程式碼

DataFlow:Dataflow型別用於處理資料流,需實現DataflowJob介面。該介面提供2個方法可供覆蓋,分別用於抓取(fetchData)和處理(processData)資料。

public class MyElasticJob implements DataflowJob<Foo> {
    
    @Override
    public List<Foo> fetchData(ShardingContext context) {
        switch (context.getShardingItem()) {
            case 0: 
                List<Foo> data = // get data from database by sharding item 0
                return data;
            case 1: 
                List<Foo> data = // get data from database by sharding item 1
                return data;
            case 2: 
                List<Foo> data = // get data from database by sharding item 2
                return data;
            // case n: ...
        }
    }
    
    @Override
    public void processData(ShardingContext shardingContext, List<Foo> data) {
        // process data
        // ...
    }
}
複製程式碼

Script:Script型別作業意為指令碼型別作業,支援shell,python,perl等所有型別指令碼。只需通過控制檯或程式碼配置scriptCommandLine即可,無需編碼。執行指令碼路徑可包含引數,引數傳遞完畢後,作業框架會自動追加最後一個引數為作業執行時資訊。

其實我建議增加一種任務型別,就是流水式任務,為此我還特意提了一個issues:

github.com/elasticjob/…

在特定的業務需求下,A任務執行完之後,需要執行B任務,以此類推,這種具有依賴性的流水式的任務。

在目前可以將這些任務合在一起,通過程式碼呼叫的方式來達到效果。

但我希望能增加這樣一個功能,比如加一個配置,job-after="com.xxx.job.XXXJob" 在執行完這個任務之後,自動呼叫另一個任務BB,BB任務只需要配置任務資訊,把cron去掉就可以,因為BB是依靠別的任務觸發執行的。

當然這些任務必須在同一個zk的名稱空間下,如果能支援誇名稱空間就更好了

這樣就能達到,流水式的任務操作了,並且每個任務可以用不同的分片key
複製程式碼

開始使用

1.關於框架怎麼搭建,怎麼配置就不做講解了,官網文件肯定比我寫的好,一般開源框架都有demo,大家下載下來匯入IDE中即可執行。

demo地址:github.com/elasticjob/…

2.介紹下使用中的一些經驗

  • 建議按產品來劃分任務,一個產品對應一個任務的專案,當團隊比較大的時候可能一個小組負責一個產品,這樣就不會跟別的混在一起了
  • 任務的描述一定要寫清楚,幹嘛用的,在配置任務中有個描述的配置,填寫清楚
/**
 * 使用者維度統計任務<br>統計出使用者的房產,置換,貸款等資訊
 * @author yinjihuan
 */
public class UserStatJob implements SimpleJob {

	private Logger logger = LoggerFactory.getLogger(UserStatJob.class);
	
	@Autowired
	private EnterpriseProductUserService enterpriseProductUserService;
	
	@Autowired
	private UserStatService userStatService;
	
	@Autowired
	private HouseInfoService houseInfoService;
	
	@Autowired
	private HouseSubstitutionService houseSubstitutionService;
	
	@Autowired
	private LoanApplyService loanApplyService;
	
	@Override
	public void execute(ShardingContext shardingContext) {
		logger.info("開始執行UserStatJob");
		long total = enterpriseProductUserService.queryCount();
		int pages = PageBean.calcPages(total, 1000);
		for (int i = 1; i <= pages; i++) {
			List<EnterpriseProductUser> users = enterpriseProductUserService.queryByPage(i, 1000);
			for (EnterpriseProductUser user : users) {
				try {
					processStat(user);
				} catch (Exception e) {
					logger.error("使用者維度統計任務異常", e);
					DingDingMessageUtil.sendTextMessage("使用者維度統計任務異常:" + e.getMessage());
				}
			}
		}
		logger.info("UserStatJob執行結束");
	}
	
	private void processStat(EnterpriseProductUser user) {
		UserStat stat = userStatService.getByUid(user.getEid(), user.getUid());
		Long eid = user.getEid();
		String uid = user.getUid();
		if (stat == null) {
			stat = new UserStat();
			stat.setEid(eid);
			stat.setUid(uid);
			stat.setUserAddTime(user.getAddTime());
			stat.setCity(user.getCity());
			stat.setRegion(user.getRegion());
		}
		stat.setHouseCount(houseInfoService.queryCountByEidAndUid(eid, uid));
		stat.setHousePrice(houseInfoService.querySumMoneyByEidAndUid(eid, uid));
		stat.setSubstitutionCount(houseSubstitutionService.queryCount(eid, uid));
		stat.setSubstitutionMaxPrice(houseSubstitutionService.queryMaxBudget(eid, uid));
		stat.setLoanEvalCount(loanApplyService.queryUserCountByType(eid, uid, 2));
		stat.setLoanEvalMaxPrice(loanApplyService.queryMaxEvalMoney(eid, uid));
		stat.setLoanCount(loanApplyService.queryUserCountByType(eid, uid, 1));
		stat.setModifyDate(new Date());
		userStatService.save(stat);
	}

}
複製程式碼
 <!-- 使用者統計任務 每天1點10分執行 -->
 <job:simple id="userStatJob" class="com.fangjia.job.fsh.job.UserStatJob" registry-center-ref="regCenter"
    	 sharding-total-count="1" cron="0 10 1 * * ?" sharding-item-parameters=""
    	 failover="true" description="【房生活】使用者維度統計任務,統計出使用者的房產,置換,貸款等資訊 UserStatJob"
    	 overwrite="true" event-trace-rdb-data-source="elasticJobLog" job-exception-handler="com.fangjia.job.fsh.handler.CustomJobExceptionHandler">
    	 
    	  <job:listener class="com.fangjia.job.fsh.listener.MessageElasticJobListener"></job:listener>
    	  
 </job:simple>
複製程式碼
  • 為每個任務配置一個統一的監聽器,來對任務的執行,結束進行通知,可以是簡訊,郵件或者別的,我這邊用的是釘釘的機器人來發訊息通知到釘釘
/**
 * 作業監聽器, 執行前後傳送釘釘訊息進行通知
 * @author yinjihuan
 */
public class MessageElasticJobListener implements ElasticJobListener {
    
    @Override
    public void beforeJobExecuted(ShardingContexts shardingContexts) {
    	String date = DateUtils.date2Str(new Date());
    	String msg = date + " 【FSH-" + shardingContexts.getJobName() + "】任務開始執行====" + JsonUtils.toJson(shardingContexts);
    	DingDingMessageUtil.sendTextMessage(msg);
    }
    
    @Override
    public void afterJobExecuted(ShardingContexts shardingContexts) {
    	String date = DateUtils.date2Str(new Date());
    	String msg = date + " 【FSH-" + shardingContexts.getJobName() + "】任務執行結束====" + JsonUtils.toJson(shardingContexts);
    	DingDingMessageUtil.sendTextMessage(msg);
    }

}
複製程式碼
  • 可以在每個任務類上定義一個註解,註解用來標識這個任務是誰開發的,然後對應的釘釘訊息就傳送給誰,我個人建議還是建一個群,然後大家都在裡面,因為如果單獨發給一個開發人員,除非他的主動性很高,不然也沒什麼用,我個人建議發在群裡,這樣領導看見了就會說那個誰誰誰,你的任務報錯了,去查下原因。我這邊是統一發的,沒有定義註解。

  • 任務的異常處理,可以在任務中對異常進行處理,除了記錄日誌,也用統一封裝好的傳送釘釘訊息來進行通知,實時知道任務是否有異常,可以檢視我上面的程式碼。

  • 還有一種是沒捕獲的異常,怎麼通知到群裡,可以自定義異常處理類來實現,
    通過配置job-exception-handler="com.fangjia.job.fsh.handler.CustomJobExceptionHandler"

/**
 * 自定義異常處理,在任務異常時使用釘釘傳送通知
 * @author yinjihuan
 */
public class CustomJobExceptionHandler implements JobExceptionHandler {
	
	private Logger logger = LoggerFactory.getLogger(CustomJobExceptionHandler.class);
	
	@Override
	public void handleException(String jobName, Throwable cause) {
		logger.error(String.format("Job `%s` exception occur in job processing", jobName), cause);
		DingDingMessageUtil.sendTextMessage("【"+jobName+"】任務異常。" + cause.getMessage());
	}

}
複製程式碼
  • 可以通過監聽job_nameinstancesjob_instance_id節點是否存在來判斷作業節點是否掛掉,該節點為臨時節點,如果作業伺服器下線,該節點將刪除。當然也可以通過其他的工具來進行監控。

  • 任務的編寫儘量考慮到水平擴充套件性,像我上面貼的那個列子其實就沒考慮到,只是一個單純的任務,因為我沒有用到shardingParameter來處理對應的片的資料,這邊其實建議大家考慮下,如果任務時間短。處理的資料少,可以寫成我這樣。如果能夠預計到未來有大量資料需要處理,而且時間很長的話最好配置下分片的規則,並且將程式碼寫成按分片來處理,這樣到了後面就直接修改配置,增加下節點就行了。

更多技術分享請關注微信公眾號:猿天地

image.png

相關文章