一、POI概述
Apache POI是Apache軟件基金會的開放源碼函式庫,POI提供API給Java程序對Microsoft Office格式檔案讀和寫的功能。
結構:
HSSF - 提供讀寫Microsoft Excel格式檔案的功能。
XSSF - 提供讀寫Microsoft Excel OOXML格式檔案的功能。
HWPF - 提供讀寫Microsoft Word格式檔案的功能。
HSLF - 提供讀寫Microsoft PowerPoint格式檔案的功能。
HDGF - 提供讀寫Microsoft Visio格式檔案的功能。
使用必須引入依賴
org.apache.poi
poi
3.17
注:3.17版本是支持jdk6的最后版本
二、HSSF概況
HSSF 是Horrible SpreadSheet Format的縮寫,通過HSSF,你可以用純Java代碼來讀取、寫入、修改Excel文件。HSSF 為讀取操作提供了兩類API:usermodel和eventusermodel,即“用戶模型”和“事件-用戶模型”。
三、 POI EXCEL文檔結構類
HSSFWorkbook excel文檔對象
HSSFSheet excel的sheet
HSSFRow excel的行
HSSFCell excel的單元格
HSSFFont excel字體
HSSFName 名稱
HSSFDataFormat 日期格式
HSSFHeader sheet頭
HSSFFooter sheet尾
HSSFCellStyle cell樣式
HSSFDateUtil 日期
HSSFPrintSetup 打印
HSSFErrorConstants 錯誤信息表
四、EXCEL的讀寫操作
1、讀取“區域數據.xls”并儲存于list集合中,“區域數據.xls”如下圖
public List
importXLS(){
ArrayList
list = new ArrayList<>();
try {
//1、獲取文件輸入流
InputStream inputStream = new FileInputStream("/Users/Shared/區域數據.xls");
//2、獲取Excel工作簿對象
HSSFWorkbook workbook = new HSSFWorkbook(inputStream);
//3、得到Excel工作表對象
HSSFSheet sheetAt = workbook.getSheetAt(0);
//4、循環讀取表格數據
for (Row row : sheetAt) {
//首行(即表頭)不讀取
if (row.getRowNum() == 0) {
continue;
}
//讀取當前行中單元格數據,索引從0開始
String areaNum = row.getCell(0).getStringCellValue();
String province = row.getCell(1).getStringCellValue();
String city = row.getCell(2).getStringCellValue();
String district = row.getCell(3).getStringCellValue();
String postcode = row.getCell(4).getStringCellValue();
Area area = new Area();
area.setCity(city);
area.setDistrict(district);
area.setProvince(province);
area.setPostCode(postcode);
list.add(area);
}
//5、關閉流
workbook.close();
} catch (IOException e) {
e.printStackTrace();
}
return list;
}
2、導出數據到“區域數據.xls”文件中,頁面數據如下圖:
public void exportExcel() throws IOException {
Page
page = areaService.pageQuery(null);
List
list = page.getContent();
//1.在內存中創建一個excel文件
HSSFWorkbook hssfWorkbook = new HSSFWorkbook();
//2.創建工作簿
HSSFSheet sheet = hssfWorkbook.createSheet();
//3.創建標題行
HSSFRow titlerRow = sheet.createRow(0);
titlerRow.createCell(0).setCellValue("省");
titlerRow.createCell(1).setCellValue("市");
titlerRow.createCell(2).setCellValue("區");
titlerRow.createCell(3).setCellValue("郵編");
titlerRow.createCell(4).setCellValue("簡碼");
titlerRow.createCell(5).setCellValue("城市編碼");
//4.遍歷數據,創建數據行
for (Area area : list) {
//獲取最后一行的行號
int lastRowNum = sheet.getLastRowNum();
HSSFRow dataRow = sheet.createRow(lastRowNum + 1);
dataRow.createCell(0).setCellValue(area.getProvince());
dataRow.createCell(1).setCellValue(area.getCity());
dataRow.createCell(2).setCellValue(area.getDistrict());
dataRow.createCell(3).setCellValue(area.getPostcode());
dataRow.createCell(4).setCellValue(area.getShortcode());
dataRow.createCell(5).setCellValue(area.getCitycode());
}
//5.創建文件名
String fileName = "區域數據統計.xls";
//6.獲取輸出流對象
HttpServletResponse response = ServletActionContext.getResponse();
ServletOutputStream outputStream = response.getOutputStream();
//7.獲取mimeType
ServletContext servletContext = ServletActionContext.getServletContext();
String mimeType = servletContext.getMimeType(fileName);
//8.獲取瀏覽器信息,對文件名進行重新編碼
HttpServletRequest request = ServletActionContext.getRequest();
fileName = FileUtils.filenameEncoding(fileName, request);
//9.設置信息頭
response.setContentType(mimeType);
response.setHeader("Content-Disposition","attachment;filename="+fileName);
//10.寫出文件,關閉流
hssfWorkbook.write(outputStream);
hssfWorkbook.close();
}
工具類
public class FileUtils {
public static String filenameEncoding(String filename, HttpServletRequest request) throws IOException {
String agent = request.getHeader("User-Agent"); //獲取瀏覽器
if (agent.contains("Firefox")) {
BASE64Encoder base64Encoder = new BASE64Encoder();
filename = "=?utf-8?B?"
+ base64Encoder.encode(filename.getBytes("utf-8"))
+ "?=";
} else if(agent.contains("MSIE")) {
filename = URLEncoder.encode(filename, "utf-8");
} else if(agent.contains ("Safari")) {
filename = new String (filename.getBytes ("utf-8"),"ISO8859-1");
} else {
filename = URLEncoder.encode(filename, "utf-8");
}
return filename;
}
}
寫出xls文件:
五、 EXCEL常用操作方法
1、 得到Excel常用對象
POIFSFileSystem fs=newPOIFSFileSystem(new FileInputStream("d:/test.xls"));
//得到Excel工作簿對象
HSSFWorkbook wb = new HSSFWorkbook(fs);
//得到Excel工作表對象
HSSFSheet sheet = wb.getSheetAt(0);
//得到Excel工作表的行
HSSFRow row = sheet.getRow(i);
//得到Excel工作表指定行的單元格
HSSFCell cell = row.getCell((short) j);
cellStyle = cell.getCellStyle();//得到單元格樣式
2、建立Excel常用對象
HSSFWorkbook wb = new HSSFWorkbook();//創建Excel工作簿對象
HSSFSheet sheet = wb.createSheet("new sheet");//創建Excel工作表對象
HSSFRow row = sheet.createRow((short)0); //創建Excel工作表的行
cellStyle = wb.createCellStyle();//創建單元格樣式
row.createCell((short)0).setCellStyle(cellStyle); //創建Excel工作表指定行的單元格
row.createCell((short)0).setCellValue(1); //設置Excel工作表的值
3、設置sheet名稱和單元格內容
wb.setSheetName(1, "第一張工作表",HSSFCell.ENCODING_UTF_16);
cell.setEncoding((short) 1);
cell.setCellValue("單元格內容");
4、取得sheet的數目
wb.getNumberOfSheets()
5、 根據index取得sheet對象
HSSFSheet sheet = wb.getSheetAt(0);
6、取得有效的行數
int rowcount = sheet.getLastRowNum();
7、取得一行的有效單元格個數
row.getLastCellNum();
8、單元格值類型讀寫
cell.setCellType(HSSFCell.CELL_TYPE_STRING); //設置單元格為STRING類型
cell.getNumericCellValue();//讀取為數值類型的單元格內容
9、設置列寬、行高
sheet.setColumnWidth((short)column,(short)width);
row.setHeight((short)height);
10、添加區域,合并單元格
Region region = new Region((short)rowFrom,(short)columnFrom,(short)rowTo
,(short)columnTo);//合并從第rowFrom行columnFrom列
sheet.addMergedRegion(region);// 到rowTo行columnTo的區域
//得到所有區域
sheet.getNumMergedRegions()
11、保存Excel文件
FileOutputStream fileOut = new FileOutputStream(path);
wb.write(fileOut);
12、根據單元格不同屬性返回字符串數值
public String getCellStringValue(HSSFCell cell) {
String cellValue = "";
switch (cell.getCellType()) {
case HSSFCell.CELL_TYPE_STRING://字符串類型
cellValue = cell.getStringCellValue();
if(cellValue.trim().equals("")||cellValue.trim().length()<=0)
cellValue=" ";
break;
case HSSFCell.CELL_TYPE_NUMERIC: //數值類型
cellValue = String.valueOf(cell.getNumericCellValue());
break;
case HSSFCell.CELL_TYPE_FORMULA: //公式
cell.setCellType(HSSFCell.CELL_TYPE_NUMERIC);
cellValue = String.valueOf(cell.getNumericCellValue());
break;
case HSSFCell.CELL_TYPE_BLANK:
cellValue=" ";
break;
case HSSFCell.CELL_TYPE_BOOLEAN:
break;
case HSSFCell.CELL_TYPE_ERROR:
break;
default:
break;
}
return cellValue;
}
13、常用單元格邊框格式
HSSFCellStyle style = wb.createCellStyle();
style.setBorderBottom(HSSFCellStyle.BORDER_DOTTED);//下邊框
style.setBorderLeft(HSSFCellStyle.BORDER_DOTTED);//左邊框
style.setBorderRight(HSSFCellStyle.BORDER_THIN);//右邊框
style.setBorderTop(HSSFCellStyle.BORDER_THIN);//上邊框
14、設置字體和內容位置
HSSFFont f = wb.createFont();
f.setFontHeightInPoints((short) 11);//字號
f.setBoldweight(HSSFFont.BOLDWEIGHT_NORMAL);//加粗
style.setFont(f);
style.setAlignment(HSSFCellStyle.ALIGN_CENTER);//左右居中
style.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);//上下居中
style.setRotation(short rotation);//單元格內容的旋轉的角度
HSSFDataFormat df = wb.createDataFormat();
style1.setDataFormat(df.getFormat("0.00%"));//設置單元格數據格式
cell.setCellFormula(string);//給單元格設公式
style.setRotation(short rotation);//單元格內容的旋轉的角度
15、插入圖片
//先把讀進來的圖片放到一個ByteArrayOutputStream中,以便產生ByteArray
ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
BufferedImage bufferImg = ImageIO.read(new File("ok.jpg"));
ImageIO.write(bufferImg,"jpg",byteArrayOut);
//讀進一個excel模版
FileInputStream fos = new FileInputStream(filePathName+"/stencil.xlt");
fs = new POIFSFileSystem(fos);
//創建一個工作薄
HSSFWorkbook wb = new HSSFWorkbook(fs);
HSSFSheet sheet = wb.getSheetAt(0);
HSSFPatriarch patriarch = sheet.createDrawingPatriarch();
HSSFClientAnchor anchor = new HSSFClientAnchor(0,0,1023,255,(short) 0,0,(short)10,10);
patriarch.createPicture(anchor , wb.addPicture(byteArrayOut.toByteArray(),HSSFWorkbook.PICTURE_TYPE_JPEG));
16、調整工作表位置
HSSFWorkbook wb = new HSSFWorkbook();
HSSFSheet sheet = wb.createSheet("format sheet");
HSSFPrintSetup ps = sheet.getPrintSetup();
sheet.setAutobreaks(true);
ps.setFitHeight((short)1);
ps.setFitWidth((short)1);
1、在學習從文件讀取數據中,寫了個示例代碼,讀取不在同一個目錄的file.txt,運行后報這個Python OSError: [Errno 22] Invalid argument:錯誤:
(1)、首先,在F盤的python_stu中新增了一個file.txt,同時在F盤的python_stu文件目錄底下新增一個file文件夾,里面有個file_reader.py來讀取python_stu文件目錄底下的file.txt,代碼分別如下:
file.txt:
測試
測試2
測試3
file_reader.py:
with open('F:\python_stu\file.txt') as file_obj:
contents = file_obj.read();
print(contents.rstrip());
(2)、運行后報錯:
(3)、出現這種錯誤的原因是由于讀取不到這個文件,看Traceback報的錯誤,最后一行,很明顯讀取不到file.txt,前面的F:\\python_stu沒錯,后面的名稱怎么變了,還是x0cile.txt。
(4)、解決辦法,可修改上述第一行代碼為:
with open('F:\python_stu/file.txt') as file_obj:
或者:
with open('F:/python_stu/file.txt') as file_obj:
或者:
with open('F://python_stu//file.txt') as file_obj:
又或者:
with open('F:\\python_stu\\file.txt') as file_obj:
還有一些我就不附上了,上面第一種方式不統一,最好不要用,用統一的方式,而且有時候還有注意一些轉義字符,比如 \t,\n也會導致報錯。
前面學習了使用命令hdfs haadmin -failover手動進行故障轉移,在該模式下,即使現役NameNode已經失效,系統也不會自動從現役NameNode轉移到待機NameNode,下面學習如何配置部署HA自動進行故障轉移。自動故障轉移為HDFS部署增加了兩個新組件:ZooKeeper和ZKFailoverController(ZKFC)進程。ZooKeeper是維護少量協調數據,通知客戶端這些數據的改變和監視客戶端故障的高可用服務。HA的自動故障轉移依賴于ZooKeeper的以下功能:
- 故障檢測:集群中的每個NameNode在ZooKeeper中維護了一個持久會話,如果機器崩潰,ZooKeeper中的會話將終止,ZooKeeper通知另一個NameNode需要觸發故障轉移。
- 現役NameNode選擇:ZooKeeper提供了一個簡單的機制用于唯一的選擇一個節點為active狀態。如果目前現役NameNode崩潰,另一個節點可能從ZooKeeper獲得特殊的排外鎖以表明它應該成為現役NameNode。
ZKFC是自動故障轉移中的另一個新組件,是ZooKeeper的客戶端,也監視和管理NameNode的狀態。每個運行NameNode的主機也運行了一個ZKFC進程,ZKFC負責:
- 健康監測:ZKFC使用一個健康檢查命令定期地ping與之在相同主機的NameNode,只要該NameNode及時地回復健康狀態,ZKFC認為該節點是健康的。如果該節點崩潰,凍結或進入不健康狀態,健康監測器標識該節點為非健康的。
- ZooKeeper會話管理:當本地NameNode是健康的,ZKFC保持一個在ZooKeeper中打開的會話。如果本地NameNode處于active狀態,ZKFC也保持一個特殊的znode鎖,該鎖使用了ZooKeeper對短暫節點的支持,如果會話終止,鎖節點將自動刪除。
- 基于ZooKeeper的選擇:如果本地NameNode是健康的,且ZKFC發現沒有其它的節點當前持有znode鎖,它將為自己獲取該鎖。如果成功,則它已經贏得了選擇,并負責運行故障轉移進程以使它的本地NameNode為active。故障轉移進城與前面描述的手動故障轉移相似,首先如果必要保護之前的現役NameNode,然后本地NameNode轉換為active狀態。
在典型部署中,ZooKeeper守護進程運行在三個或者五個節點上,但由于ZooKeeper本身需要較少的資源,所以將ZooKeeper部署在與現役NameNode和待機NameNode相同的主機上,還可以將ZooKeeper部署到與YARN的ResourceManager相同的節點上。建議配置ZooKeeper將數據存儲在與HDFS元數據不同的硬盤上以得到最好的性能和隔離性。在配置自動故障轉移之前需要先停掉集群,目前在集群運行時還不可能將手動故障轉移的安裝轉換為自動故障轉移的安裝。接下來看看如何配置HA的自動故障轉移。首先在hdfs-site.xml中添加下面的參數,該參數的值默認為false:
- <property>
- <name>dfs.ha.automatic-failover.enabled</name>
- <value>true</value>
- </property>
在core-site.xml文件中添加下面的參數,該參數的值為ZooKeeper服務器的地址,ZKFC將使用該地址。
- <property>
- <name>ha.zookeeper.quorum</name> <value>zk1.example.com:2181,zk2.example.com:2181,zk3.example.com:2181</value>
- </property>
在HA或者HDFS聯盟中,上面的兩個參數還需要以NameServiceID為后綴,比如dfs.ha.automatic-failover.enabled.mycluster。除了上面的兩個參數外,還有其它幾個參數用于自動故障轉移,比如ha.zookeeper.session-timeout.ms,但對于大多數安裝來說都不是必須的。
在添加了上述的配置參數后,下一步就是在ZooKeeper中初始化要求的狀態,可以在任一NameNode中運行下面的命令實現該目的,該命令將在ZooKeeper中創建znode:
在啟用自動故障轉移的集群中,start-dfs.sh腳本將在任何運行NameNode的主機上自動啟動ZKFC守護進程,一旦ZKFC啟動完畢,它們將自動選擇一個NameNode為現役NameNode。如果手動管理集群中的服務,需要在每臺運行NameNode的主機上手動啟動ZKFC,命令為:
- hadoop-daemon.sh start zkfc
- hdfs zkfc
如果正在運行一個安全的集群,可能想確保存儲在ZooKeeper中的信息也是安全的,這將阻止惡意的客戶端修改ZooKeeper中的元數據或者潛在地觸發一個錯誤的故障轉移。為了保護ZooKeeper中的信息,首先在core-site.xml中添加下面的參數:
- <property>
- <name>ha.zookeeper.auth</name>
- <value>@/path/to/zk-auth.txt</value>
- </property>
- <property>
- <name>ha.zookeeper.acl</name>
- <value>@/path/to/zk-acl.txt</value>
- </property>
參數值中的@字符表示參數值保存在@后的硬盤文件中。第一個配置文件指定了ZooKeeper的認證列表,其格式與ZK CLI使用的相同,例如:digest:hdfs-zkfcs:mypassword,其中hdfs-zkfcs為ZooKeeper的用戶名,mypassword為密碼。其次使用下面的命令為該認證生成一個ZooKeeper訪問控制列表:
- $ java -cp $ZK_HOME/lib/*:$ZK_HOME/zookeeper-3.4.2.jar org.apache.zookeeper.server.auth.DigestAuthenticationProvider hdfs-zkfcs:mypassword
- output: hdfs-zkfcs:mypassword->hdfs-zkfcs:P/OQvnYyU/nF/mGYvB/xurX8dYs=
拷貝->之后的字符串并添加digest:前綴,然后粘貼到zk-acls.txt中,例如:digest:hdfs-zkfcs:vlUvLnd8MlacsE80rDuu6ONESbM=:rwcda。要想使ACLs生效,需要再次運行zkfc –formatZK。最后可能像下面這樣在ZK CLI中驗證ACLs:
- [zk: localhost:2181(CONNECTED) 1] getAcl /hadoop-ha
- 'digest,'hdfs-zkfcs:vlUvLnd8MlacsE80rDuu6ONESbM=
- : cdrwa
在安裝完成自動故障轉移后,或許需要測試一下。首先定位現役NameNode,可以通過訪問NameNode的web頁面來確定哪個NameNode是active狀態的。一旦確定了處于active狀態的NameNode,就需要在該節點上制造點故障,比如使用命令kill -9 <pid of NN>模擬JVM崩潰,或重啟主機或拔掉網線來模擬不同的中斷。一旦觸發了自動故障轉移,另一個NameNode應該自動在幾秒鐘內變為active狀態。檢測到故障并觸發故障轉移由參數ha.zookeeper.session-timeout.ms控制,該參數為與core-site.xml中,默認為5秒。如果測試不成功,可能是配置問題,檢查ZKFC和NameNode進程的日志以進一步診斷問題,通常錯誤都是很明顯的。
理想情況下,我們應用對Yarn資源的請求應該立刻得到滿足,但現實情況資源往往是有限的,特別是在一個很繁忙的集群,一個應用資源的請求經常需要等待一段時間才能的到相應的資源。在Yarn中,負責給應用分配資源的就是Scheduler。其實調度本身就是一個難題,很難找到一個完美的策略可以解決所有的應用場景。為此,Yarn提供了多種調度器和可配置的策略供我們選擇。
一、調度器的選擇
在Yarn中有三種調度器可以選擇:FIFO Scheduler
,Capacity Scheduler
,FairS cheduler
。
FIFO Scheduler
把應用按提交的順序排成一個隊列,這是一個先進先出隊列,在進行資源分配的時候,先給隊列中最頭上的應用進行分配資源,待最頭上的應用需求滿足后再給下一個分配,以此類推。
FIFO Scheduler
是最簡單也是最容易理解的調度器,也不需要任何配置,但它并不適用于共享集群。大的應用可能會占用所有集群資源,這就導致其它應用被阻塞。在共享集群中,更適合采用Capacity Scheduler
或Fair Scheduler
,這兩個調度器都允許大任務和小任務在提交的同時獲得一定的系統資源。
下面“Yarn調度器對比圖”展示了這幾個調度器的區別,從圖中可以看出,在FIFO 調度器中,小任務會被大任務阻塞。
而對于Capacity調度器,有一個專門的隊列用來運行小任務,但是為小任務專門設置一個隊列會預先占用一定的集群資源,這就導致大任務的執行時間會落后于使用FIFO調度器時的時間。
在Fair調度器中,我們不需要預先占用一定的系統資源,Fair調度器會為所有運行的job動態的調整系統資源。如下圖所示,當第一個大job提交時,只有這一個job在運行,此時它獲得了所有集群資源;當第二個小任務提交后,Fair調度器會分配一半資源給這個小任務,讓這兩個任務公平的共享集群資源。
需要注意的是,在下圖Fair調度器中,從第二個任務提交到獲得資源會有一定的延遲,因為它需要等待第一個任務釋放占用的Container。小任務執行完成之后也會釋放自己占用的資源,大任務又獲得了全部的系統資源。最終的效果就是Fair調度器即得到了高的資源利用率又能保證小任務及時完成。
Yarn調度器對比圖:

二、Capacity Scheduler(容器調度器)的配置
2.1 容器調度介紹
Capacity 調度器允許多個組織共享整個集群,每個組織可以獲得集群的一部分計算能力。通過為每個組織分配專門的隊列,然后再為每個隊列分配一定的集群資源,這樣整個集群就可以通過設置多個隊列的方式給多個組織提供服務了。除此之外,隊列內部又可以垂直劃分,這樣一個組織內部的多個成員就可以共享這個隊列資源了,在一個隊列內部,資源的調度是采用的是先進先出(FIFO)策略。
通過上面那幅圖,我們已經知道一個job可能使用不了整個隊列的資源。然而如果這個隊列中運行多個job,如果這個隊列的資源夠用,那么就分配給這些job,如果這個隊列的資源不夠用了呢?其實Capacity調度器仍可能分配額外的資源給這個隊列,這就是“彈性隊列”(queue elasticity)的概念。
在正常的操作中,Capacity調度器不會強制釋放Container,當一個隊列資源不夠用時,這個隊列只能獲得其它隊列釋放后的Container資源。當然,我們可以為隊列設置一個最大資源使用量,以免這個隊列過多的占用空閑資源,導致其它隊列無法使用這些空閑資源,這就是”彈性隊列”需要權衡的地方。
2.2 容器調度的配置
假設我們有如下層次的隊列:
root ├── prod └── dev ├── eng └── science
下面是一個簡單的Capacity調度器的配置文件,文件名為capacity-scheduler.xml
。在這個配置中,在root隊列下面定義了兩個子隊列prod
和dev
,分別占40%和60%的容量。需要注意,一個隊列的配置是通過屬性yarn.sheduler.capacity.<queue-path>.<sub-property>
指定的,<queue-path>
代表的是隊列的繼承樹,如root.prod
隊列,<sub-property>
一般指capacity
和maximum-capacity
。

我們可以看到,dev
隊列又被分成了eng
和science
兩個相同容量的子隊列。dev
的maximum-capacity
屬性被設置成了75%,所以即使prod
隊列完全空閑dev
也不會占用全部集群資源,也就是說,prod
隊列仍有25%的可用資源用來應急。我們注意到,eng
和science
兩個隊列沒有設置maximum-capacity
屬性,也就是說eng
或science
隊列中的job可能會用到整個dev
隊列的所有資源(最多為集群的75%)。而類似的,prod
由于沒有設置maximum-capacity屬性,它有可能會占用集群全部資源。
Capacity容器除了可以配置隊列及其容量外,我們還可以配置一個用戶或應用可以分配的最大資源數量、可以同時運行多少應用、隊列的ACL認證等。
2.3 隊列的設置
關于隊列的設置,這取決于我們具體的應用。比如,在MapReduce中,我們可以通過mapreduce.job.queuename
屬性指定要用的隊列。如果隊列不存在,我們在提交任務時就會收到錯誤。如果我們沒有定義任何隊列,所有的應用將會放在一個default
隊列中。
注意:對于Capacity調度器,我們的隊列名必須是隊列樹中的最后一部分,如果我們使用隊列樹則不會被識別。比如,在上面配置中,我們使用prod
和eng
作為隊列名是可以的,但是如果我們用root.dev.eng
或者dev.eng
是無效的。
三、Fair Scheduler(公平調度器)的配置
3.1 公平調度
Fair調度器的設計目標是為所有的應用分配公平的資源(對公平的定義可以通過參數來設置)。在上面的“Yarn調度器對比圖”展示了一個隊列中兩個應用的公平調度;當然,公平調度在也可以在多個隊列間工作。舉個例子,假設有兩個用戶A和B,他們分別擁有一個隊列。當A啟動一個job而B沒有任務時,A會獲得全部集群資源;當B啟動一個job后,A的job會繼續運行,不過一會兒之后兩個任務會各自獲得一半的集群資源。如果此時B再啟動第二個job并且其它job還在運行,則它將會和B的第一個job共享B這個隊列的資源,也就是B的兩個job會用于四分之一的集群資源,而A的job仍然用于集群一半的資源,結果就是資源最終在兩個用戶之間平等的共享。過程如下圖所示:

3.2 啟用Fair Scheduler
調度器的使用是通過yarn-site.xml
配置文件中的yarn.resourcemanager.scheduler.class
參數進行配置的,默認采用Capacity Scheduler調度器。如果我們要使用Fair調度器,需要在這個參數上配置FairScheduler類的全限定名: org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler
。
3.3 隊列的配置
Fair調度器的配置文件位于類路徑下的fair-scheduler.xml
文件中,這個路徑可以通過yarn.scheduler.fair.allocation.file
屬性進行修改。若沒有這個配置文件,Fair調度器采用的分配策略,這個策略和3.1節介紹的類似:調度器會在用戶提交第一個應用時為其自動創建一個隊列,隊列的名字就是用戶名,所有的應用都會被分配到相應的用戶隊列中。
我們可以在配置文件中配置每一個隊列,并且可以像Capacity 調度器一樣分層次配置隊列。比如,參考capacity-scheduler.xml
來配置fair-scheduler:

隊列的層次是通過嵌套<queue>
元素實現的。所有的隊列都是root
隊列的孩子,即使我們沒有配到<root>
元素里。在這個配置中,我們把dev
隊列有分成了eng
和science
兩個隊列。
Fair調度器中的隊列有一個權重屬性(這個權重就是對公平的定義),并把這個屬性作為公平調度的依據。在這個例子中,當調度器分配集群40:60
資源給prod
和dev
時便視作公平,eng
和science
隊列沒有定義權重,則會被平均分配。這里的權重并不是百分比,我們把上面的40和60分別替換成2和3,效果也是一樣的。注意,對于在沒有配置文件時按用戶自動創建的隊列,它們仍有權重并且權重值為1。
每個隊列內部仍可以有不同的調度策略。隊列的默認調度策略可以通過頂級元素<defaultQueueSchedulingPolicy>
進行配置,如果沒有配置,默認采用公平調度。
盡管是Fair調度器,其仍支持在隊列級別進行FIFO調度。每個隊列的調度策略可以被其內部的<schedulingPolicy>
元素覆蓋,在上面這個例子中,prod
隊列就被指定采用FIFO進行調度,所以,對于提交到prod
隊列的任務就可以按照FIFO規則順序的執行了。需要注意,prod
和dev
之間的調度仍然是公平調度,同樣eng
和science
也是公平調度。
盡管上面的配置中沒有展示,每個隊列仍可配置最大、最小資源占用數和最大可運行的應用的數量。
3.4 隊列的設置
Fair調度器采用了一套基于規則的系統來確定應用應該放到哪個隊列。在上面的例子中,<queuePlacementPolicy>
元素定義了一個規則列表,其中的每個規則會被逐個嘗試直到匹配成功。例如,上例第一個規則specified
,則會把應用放到它指定的隊列中,若這個應用沒有指定隊列名或隊列名不存在,則說明不匹配這個規則,然后嘗試下一個規則。primaryGroup
規則會嘗試把應用放在以用戶所在的Unix組名命名的隊列中,如果沒有這個隊列,不創建隊列轉而嘗試下一個規則。當前面所有規則不滿足時,則觸發default
規則,把應用放在dev.eng
隊列中。
當然,我們可以不配置queuePlacementPolicy
規則,調度器則默認采用如下規則:
<queuePlacementPolicy> <rule name="specified" /> <rule name="user" /> </queuePlacementPolicy>
上面規則可以歸結成一句話,除非隊列被準確的定義,否則會以用戶名為隊列名創建隊列。
還有一個簡單的配置策略可以使得所有的應用放入同一個隊列(default),這樣就可以讓所有應用之間平等共享集群而不是在用戶之間。這個配置的定義如下:
<queuePlacementPolicy> <rule name="default" /> </queuePlacementPolicy>
實現上面功能我們還可以不使用配置文件,直接設置yarn.scheduler.fair.user-as-default-queue=false
,這樣應用便會被放入default 隊列,而不是各個用戶名隊列。另外,我們還可以設置yarn.scheduler.fair.allow-undeclared-pools=false
,這樣用戶就無法創建隊列了。
3.5 搶占(Preemption)
當一個job提交到一個繁忙集群中的空隊列時,job并不會馬上執行,而是阻塞直到正在運行的job釋放系統資源。為了使提交job的執行時間更具預測性(可以設置等待的超時時間),Fair調度器支持搶占。
搶占就是允許調度器殺掉占用超過其應占份額資源隊列的containers,這些containers資源便可被分配到應該享有這些份額資源的隊列中。需要注意搶占會降低集群的執行效率,因為被終止的containers需要被重新執行。
可以通過設置一個全局的參數yarn.scheduler.fair.preemption=true
來啟用搶占功能。此外,還有兩個參數用來控制搶占的過期時間(這兩個參數默認沒有配置,需要至少配置一個來允許搶占Container):
- minimum share preemption timeout - fair share preemption timeout
如果隊列在minimum share preemption timeout
指定的時間內未獲得最小的資源保障,調度器就會搶占containers。我們可以通過配置文件中的頂級元素<defaultMinSharePreemptionTimeout>
為所有隊列配置這個超時時間;我們還可以在<queue>
元素內配置<minSharePreemptionTimeout>
元素來為某個隊列指定超時時間。
與之類似,如果隊列在fair share preemption timeout
指定時間內未獲得平等的資源的一半(這個比例可以配置),調度器則會進行搶占containers。這個超時時間可以通過頂級元素<defaultFairSharePreemptionTimeout>
和元素級元素<fairSharePreemptionTimeout>
分別配置所有隊列和某個隊列的超時時間。上面提到的比例可以通過<defaultFairSharePreemptionThreshold>
(配置所有隊列)和<fairSharePreemptionThreshold>
(配置某個隊列)進行配置,默認是0.5。