大數據面試題130道及答案整理 1-15



1、HashMap 和 Hashtable 區別 

 HashMap和Hashtable都實現了Map接口,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。

HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)。

HashMap是非synchronized,而Hashtable是synchronized,這意味着Hashtable是線程安全的,多個線程可以共享一個Hashtable;而如果沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。

另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它線程改變了HashMap的結構(增加或者移除元素),將會拋出ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別。

由於Hashtable是線程安全的也是synchronized,所以在單線程環境下它比HashMap要慢。如果你不需要同步,只需要單一線程,那么使用HashMap性能要好過Hashtable。

HashMap不能保證隨着時間的推移Map中的元素次序是不變的。

要注意的一些重要術語:

sychronized意味着在一次僅有一個線程能夠更改Hashtable。就是說任何線程要更新Hashtable時要首先獲得同步鎖,其它線程要等到同步鎖被釋放之后才能再次獲得同步鎖更新Hashtable。

Fail-safe和iterator迭代器相關。如果某個集合對象創建了Iterator或者ListIterator,然后其它的線程試圖“結構上”更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程可以通過set()方法更改集合對象是允許的,因為這並沒有從“結構上”更改集合。但是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。

結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。

我們能否讓HashMap同步?

HashMap可以通過下面的語句進行同步:

Map m = Collections.synchronizeMap(hashMap);

結論

Hashtable和HashMap有幾個主要的不同:線程安全以及速度。僅在你需要完全的線程安全的時候使用Hashtable,而如果你使用Java 5或以上的話,請使用ConcurrentHashMap吧。

2、Java 垃圾回收機制和生命周期 

C語言:

 
 

Java語言:

 
 

c的垃圾回收是人工的,工作量大,但是可控性高。

java是自動化的,但是可控性很差,甚至有時會出現內存溢出的情況,

內存溢出也就是jvm分配的內存中對象過多,超出了最大可分配內存的大小。

提到java的垃圾回收機制就不得不提一個方法: ​  

System.gc()用於調用垃圾收集器,在調用時,垃圾收集器將運行以回收未使用的內存空間。它將嘗試釋放被丟棄對象占用的內存。

然而System.gc()調用附帶一個免責聲明,無法保證對垃圾收集器的調用。

所以System.gc()並不能說是完美主動進行了垃圾回收。

作為java程序員還是很有必要了解一下gc,這也是面試過程中經常出現的一道題目。

 我們從三個角度來理解gc。

 1jvm怎么確定哪些對象應該進行回收

 2jvm會在什么時候進行垃圾回收的動作

 3jvm到底是怎么清楚垃圾對象的

jvm怎么確定哪些對象應該進行回收

對象是否會被回收的兩個經典算法:引用計數法,和可達性分析算法。

引用計數法

簡單的來說就是判斷對象的引用數量。實現方式:給對象共添加一個引用計數器,每當有引用對他進行引用時,計數器的值就加1,當引用失效,也就是不在執行此對象是,他的計數器的值就減1,若某一個對象的計數器的值為0,那么表示這個對象沒有人對他進行引用,也就是意味着是一個失效的垃圾對象,就會被gc進行回收。

 但是這種簡單的算法在當前的jvm中並沒有采用,原因是他並不能解決對象之間循環引用的問題。

 假設有A和B兩個對象之間互相引用,也就是說A對象中的一個屬性是B,B中的一個屬性時A,這種情況下由於他們的相互引用,從而是垃圾回收機制無法識別。

 
 

因為引用計數法的缺點有引入了可達性分析算法,通過判斷對象的引用鏈是否可達來決定對象是否可以被回收。可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關系看作一張圖,通過一系列的名為GC Roots的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連(就是從 GC Roots 到這個對象不可達)時,則證明此對象是不可用的。

如圖:

 
 

二在確定了哪些對象可以被回收之后,jvm會在什么時候進行回收

 1會在cpu空閑的時候自動進行回收

 2在堆內存存儲滿了之后

 3主動調用System.gc()后嘗試進行回收

三如何回收

 如何回收說的也就是垃圾收集的算法。

算法又有四個:標記-清除算法,復制算法,標記-整理算法,分代收集算法.

 1 標記-清除算法。

 這是最基礎的一種算法,分為兩個步驟,第一個步驟就是標記,也就是標記處所有需要回收的對象,標記完成后就進行統一的回收掉哪些帶有標記的對象。這種算法優點是簡單,缺點是效率問題,還有一個最大的缺點是空間問題,標記清除之后會產生大量不連續的內存碎片,當程序在以后的運行過程中需要分配較大對象時無法找到足夠的連續內存而造成內存空間浪費。

執行如圖:

 
 

2復制算法。

復制將可用內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況。只是這種算法的代價是將內存縮小為原來的一半。

復制算法的執行過程如圖:

 
 

復制收集算法在對象存活率較高時就要執行較多的復制操作,效率將會變低。更關鍵的是,浪費了一半的空間。

標記-整理算法:

標記整理算法與標記清除算法很相似,但最顯著的區別是:標記清除算法僅對不存活的對象進行處理,剩余存活對象不做任何處理,造成內存碎片;而標記整理算法不僅對不存活對象進行處理清除,還對剩余的存活對象進行整理,重新整理,因此其不會產生內存碎片。

 
 

分代收集算法:

分代收集算法是一種比較智能的算法,也是現在jvm使用最多的一種算法,他本身其實不是一個新的算法,而是他會在具體的場景自動選擇以上三種算法進行垃圾對象回收。

那么現在的重點就是分代收集算法中說的自動根據具體場景進行選擇。這個具體場景到底是什么場景。

場景其實指的是針對jvm的哪一個區域,1.7之前jvm把內存分為三個區域:新生代,老年代,永久代。

 
 

了解過場景之后再結合分代收集算法得出結論: 1、在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法。只需要付出少量存活對象的復制成本就可以完成收集。 2、老年代中因為對象存活率高、沒有額外空間對他進行分配擔保,就必須用標記-清除或者標記-整理。

 
 

注意:

在jdk8的時候java廢棄了永久代,但是並不意味着我們以上的結論失效,因為java提供了與永久代類似的叫做“元空間”的技術。

廢棄永久代的原因:由於永久代內存經常不夠用或發生內存泄露,爆出異常java.lang.OutOfMemoryErroy。元空間的本質和永久代類似。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。也就是不局限與jvm可以使用系統的內存。理論上取決於32位/64位系統可虛擬的內存大小。

GC垃圾回收:

    jvm按照對象的生命周期,將內存按“代”划分(將堆划分為多個地址池):新生代、老年代和持久代(jdk1.8后移除持久代);

    在JVM中程序(PC)計數器、JAVA棧、本地方法棧3個區域隨線程而生、隨線程而滅,因此這幾個區域的內存分配和回收都具備確定性,就不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟隨着回收了。而堆和方法區則不一樣,這部分內存的分配和回收是動態的,正是垃圾收集器所需關注的部分。

    java中新創建的對象會先被放在新生代區域,該區域對象使用頻繁,jvm會在該區域使用不同算法回收一定的短期對象,如果某些對象使用次數達到一定限制后,那么該對象就會被放入老年代區域,老年代區域要比新生代區域更大一些(堆內存大部分分配給了老年代區域),而持久代保存的是類的元數據、常量、類靜態變量等。  

方法區和永久代的區別:

   對於方法區和永久代的區別的話,人們一直將它們看作一個部件,其實永久代實現了方法區,比作java中類的話,永久代就是接口實現類,方法區就是接口。

finalize()和System.gc()方法介紹:

    提到GC就要提到finalize()方法,該方法是在jvm確定了一個對象符合GC的條件下執行的,用於對一些外部資源的釋放等操作,但是何時對這個對象回收我們就不知道了;需要注意的是在jvm調用了該方法后,這個符合GC的對象也不一定最后就被回收了,因為在執行了finalize()方法后由於在方法體給對該方法進行了一些操作,使得該對象不符合GC的條件,例如將一個引用指向這個對象,最終導致該對象不會被GC,但這也只能求這個對象依次。

    同樣還有System.gc()方法,這個方法的調用,jvm也不會立即執行對對象的回收,gc()僅僅是提醒jvm可以回收該方法了,但實際上要根據jvm內存需求來確定何實回收這個可以回收的對象。

那么gc()和finalize()的區別是什么呢?

    首先finalize()方法是jvm調用的,但是在回收期間不一定每個對象都會調用這個方法進行收尾工作,這也是這個方法不被提倡使用的原因。而System.gc()方法可以人為調用進行標記一個對象可以被回收。

最后我們從何時回收對象比較,finalize()標記的對象是在被標記后的第二次回收時進行回收,而System.gc()方法沒有這種規定,它只是被標記,何時回收由jvm決定。

代碼示例:

public class Test {

@Override

protected void finalize() throws Throwable {

super.finalize();

System.out.println("調用");

}

public static void main(String[] args){

Test test = new Test();

test=null;

System.gc();

}

}

分析:

    我們這里創建了Test類並重寫了finalize()方法,然后我在主方法里創建了一個Test對象,並使其引用為空(此時符合回收條件)我們先調用System.gc()

結果:

    調用

    我們發現執行了finalize()方法,OK,我們現在將System.gc()注釋掉,我們會發現並沒有輸出“調用”,也就是沒有調用finalize()方法,這就是不一定每個垃圾對象jvm都會自動調用finalize()方法。

3、怎么解決 Kafka 數據丟失的問題 

1)消費端弄丟了數據

唯一可能導致消費者弄丟數據的情況,就是說,你那個消費到了這個消息,然后消費者那邊自動提交了offset,讓kafka以為你已經消費好了這個消息,其實你剛准備處理這個消息,你還沒處理,你自己就掛了,此時這條消息就丟咯。

這不是一樣么,大家都知道kafka會自動提交offset,那么只要關閉自動提交offset,在處理完之后自己手動提交offset,就可以保證數據不會丟。但是此時確實還是會重復消費,比如你剛處理完,還沒提交offset,結果自己掛了,此時肯定會重復消費一次,自己保證冪等性就好了。

生產環境碰到的一個問題,就是說我們的kafka消費者消費到了數據之后是寫到一個內存的queue里先緩沖一下,結果有的時候,你剛把消息寫入內存queue,然后消費者會自動提交offset。

然后此時我們重啟了系統,就會導致內存queue里還沒來得及處理的數據就丟失了

2)kafka弄丟了數據

這塊比較常見的一個場景,就是kafka某個broker宕機,然后重新選舉partiton的leader時。大家想想,要是此時其他的follower剛好還有些數據沒有同步,結果此時leader掛了,然后選舉某個follower成leader之后,他不就少了一些數據?這就丟了一些數據啊。

生產環境也遇到過,我們也是,之前kafka的leader機器宕機了,將follower切換為leader之后,就會發現說這個數據就丟了

所以此時一般是要求起碼設置如下4個參數:

給這個topic設置replication.factor參數:這個值必須大於1,要求每個partition必須有至少2個副本

在kafka服務端設置min.insync.replicas參數:這個值必須大於1,這個是要求一個leader至少感知到有至少一個follower還跟自己保持聯系,沒掉隊,這樣才能確保leader掛了還有一個follower吧

在producer端設置acks=all:這個是要求每條數據,必須是寫入所有replica之后,才能認為是寫成功了

在producer端設置retries=MAX(很大很大很大的一個值,無限次重試的意思):這個是要求一旦寫入失敗,就無限重試,卡在這里了

我們生產環境就是按照上述要求配置的,這樣配置之后,至少在kafka broker端就可以保證在leader所在broker發生故障,進行leader切換時,數據不會丟失

3)生產者會不會弄丟數據

如果按照上述的思路設置了ack=all,一定不會丟,要求是,你的leader接收到消息,所有的follower都同步到了消息之后,才認為本次寫成功了。如果沒滿足這個條件,生產者會自動不斷的重試,重試無限次。

4、zookeeper 是如何保證數據一致性的 

ZooKeeper是個集群,內部有多個server,每個server都可以連接多個client,每個client都可以修改server中的數據

ZooKeeper可以保證每個server內的數據完全一致,是如何實現的呢?

答:數據一致性是靠Paxos算法保證的,Paxos可以說是分布式一致性算法的鼻祖,是ZooKeeper的基礎

Paxos的基本思路:(深入解讀zookeeper一致性原理)

假設有一個社團,其中有團員、議員(決議小組成員)兩個角色

團員可以向議員申請提案來修改社團制度

議員坐在一起,拿出自己收到的提案,對每個提案進行投票表決,超過半數通過即可生效

為了秩序,規定每個提案都有編號ID,按順序自增

每個議員都有一個社團制度筆記本,上面記着所有社團制度,和最近處理的提案編號,初始為0

投票通過的規則:

新提案ID 是否大於 議員本中的ID,是議員舉手贊同

如果舉手人數大於議員人數的半數,即讓新提案生效

例如:

剛開始,每個議員本子上的ID都為0,現在有一個議員拿出一個提案:團費降為100元,這個提案的ID自增為1

每個議員都和自己ID對比,一看 1>0,舉手贊同,同時修改自己本中的ID為1

發出提案的議員一看超過半數同意,就宣布:1號提案生效

然后所有議員都修改自己筆記本中的團費為100元

以后任何一個團員咨詢任何一個議員:"團費是多少?",議員可以直接打開筆記本查看,並回答:團費為100元

可能會有極端的情況,就是多個議員一起發出了提案,就是並發的情況

例如

剛開始,每個議員本子上的編號都為0,現在有兩個議員(A和B)同時發出了提案,那么根據自增規則,這兩個提案的編號都為1,但只會有一個被先處理

假設A的提案在B的上面,議員們先處理A提案並通過了,這時,議員們的本子上的ID已經變為了1,接下來處理B的提案,由於它的ID是1,不大於議員本子上的ID,B提案就被拒絕了,B議員需要重新發起提案

上面就是Paxos的基本思路,對照ZooKeeper,對應關系就是:

團員 -client

議員 -server

議員的筆記本 -server中的數據

提案 -變更數據的請求

提案編號 -zxid(ZooKeeper Transaction Id)

提案生效 -執行變更數據的操作

ZooKeeper中還有一個leader的概念,就是把發起提案的權利收緊了,以前是每個議員都可以發起提案,現在有了leader,大家就不要七嘴八舌了,先把提案都交給leader,由leader一個個發起提案

Paxos算法就是通過投票、全局編號機制,使同一時刻只有一個寫操作被批准,同時並發的寫操作要去爭取選票,只有獲得過半數選票的寫操作才會被批准,所以永遠只會有一個寫操作得到批准,其他的寫操作競爭失敗只好再發起一輪投票

1)一致性保證

Zookeeper是一種高性能、可擴展的服務。Zookeeper的讀寫速度非常快,並且讀的速度要比寫的速度更快。另外,在進行讀操作的時候,ZooKeeper依然能夠為舊的數據提供服務。這些都是由於ZooKeepe所提供的一致性保證,它具有如下特點:

 順序一致性

客戶端的更新順序與它們被發送的順序相一致。

原子性

更新操作要么成功要么失敗,沒有第三種結果。

單系統鏡像

無論客戶端連接到哪一個服務器,客戶端將看到相同的ZooKeeper視圖。

 可靠性

一旦一個更新操作被應用,那么在客戶端再次更新它之前,它的值將不會改變。。這個保證將會產生下面兩種結果:

1.如果客戶端成功地獲得了正確的返回代碼,那么說明更新已經成果。如果不能夠獲得返回代碼(由於通信錯誤、超時等等),那么客戶端將不知道更新操作是否生效。

2.當從故障恢復的時候,任何客戶端能夠看到的執行成功的更新操作將不會被回滾。

 實時性

在特定的一段時間內,客戶端看到的系統需要被保證是實時的(在十幾秒的時間里)。在此時間段內,任何系統的改變將被客戶端看到,或者被客戶端偵測到。

給予這些一致性保證,ZooKeeper更高級功能的設計與實現將會變得非常容易,例如:leader選舉、隊列以及可撤銷鎖等機制的實現。

2)Leader選舉

ZooKeeper需要在所有的服務(可以理解為服務器)中選舉出一個Leader,然后讓這個Leader來負責管理集群。此時,集群中的其它服務器則成為此Leader的Follower。並且,當Leader故障的時候,需要ZooKeeper能夠快速地在Follower中選舉出下一個Leader。這就是ZooKeeper的Leader機制,下面我們將簡單介紹在ZooKeeper中,Leader選舉(Leader Election)是如何實現的。

此操作實現的核心思想是:首先創建一個EPHEMERAL目錄節點,例如“/election”。然后。每一個ZooKeeper服務器在此目錄下創建一個SEQUENCE|EPHEMERAL 類型的節點,例如“/election/n_”。在SEQUENCE標志下,ZooKeeper將自動地為每一個ZooKeeper服務器分配一個比前一個分配的序號要大的序號。此時創建節點的ZooKeeper服務器中擁有最小序號編號的服務器將成為Leader。

在實際的操作中,還需要保障:當Leader服務器發生故障的時候,系統能夠快速地選出下一個ZooKeeper服務器作為Leader。一個簡單的解決方案是,讓所有的follower監視leader所對應的節點。當Leader發生故障時,Leader所對應的臨時節點將會自動地被刪除,此操作將會觸發所有監視Leader的服務器的watch。這樣這些服務器將會收到Leader故障的消息,並進而進行下一次的Leader選舉操作。但是,這種操作將會導致“從眾效應”的發生,尤其當集群中服務器眾多並且帶寬延遲比較大的時候,此種情況更為明顯。

在Zookeeper中,為了避免從眾效應的發生,它是這樣來實現的:每一個follower對follower集群中對應的比自己節點序號小一號的節點(也就是所有序號比自己小的節點中的序號最大的節點)設置一個watch。只有當follower所設置的watch被觸發的時候,它才進行Leader選舉操作,一般情況下它將成為集群中的下一個Leader。很明顯,此Leader選舉操作的速度是很快的。因為,每一次Leader選舉幾乎只涉及單個follower的操作。

5、hadoop 和 spark 在處理數據時,處理出現內存溢出的方法有哪些?

1. map過程產生大量對象導致內存溢出

這種溢出的原因是在單個map中產生了大量的對象導致的。

例如:rdd.map(x=>for(i <- 1 to 10000) yield i.toString),這個操作在rdd中,每個對象都產生了10000個對象,這肯定很容易產生內存溢出的問題。針對這種問題,在不增加內存的情況下,可以通過減少每個Task的大小,以便達到每個Task即使產生大量的對象Executor的內存也能夠裝得下。具體做法可以在會產生大量對象的map操作之前調用repartition方法,分區成更小的塊傳入map。例如:rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)。

面對這種問題注意,不能使用rdd.coalesce方法,這個方法只能減少分區,不能增加分區,不會有shuffle的過程。

2.數據不平衡導致內存溢出

數據不平衡除了有可能導致內存溢出外,也有可能導致性能的問題,解決方法和上面說的類似,就是調用repartition重新分區。這里就不再累贅了。

3.coalesce調用導致內存溢出

這是我最近才遇到的一個問題,因為hdfs中不適合存小問題,所以Spark計算后如果產生的文件太小,我們會調用coalesce合並文件再存入hdfs中。但是這會導致一個問題,例如在coalesce之前有100個文件,這也意味着能夠有100個Task,現在調用coalesce(10),最后只產生10個文件,因為coalesce並不是shuffle操作,這意味着coalesce並不是按照我原本想的那樣先執行100個Task,再將Task的執行結果合並成10個,而是從頭到位只有10個Task在執行,原本100個文件是分開執行的,現在每個Task同時一次讀取10個文件,使用的內存是原來的10倍,這導致了OOM。解決這個問題的方法是令程序按照我們想的先執行100個Task再將結果合並成10個文件,這個問題同樣可以通過repartition解決,調用repartition(10),因為這就有一個shuffle的過程,shuffle前后是兩個Stage,一個100個分區,一個是10個分區,就能按照我們的想法執行。

4.shuffle后內存溢出

shuffle內存溢出的情況可以說都是shuffle后,單個文件過大導致的。在Spark中,join,reduceByKey這一類型的過程,都會有shuffle的過程,在shuffle的使用,需要傳入一個partitioner,大部分Spark中的shuffle操作,默認的partitioner都是HashPatitioner,默認值是父RDD中最大的分區數,這個參數通過spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions) , spark.default.parallelism參數只對HashPartitioner有效,所以如果是別的Partitioner或者自己實現的Partitioner就不能使用spark.default.parallelism這個參數來控制shuffle的並發量了。如果是別的partitioner導致的shuffle內存溢出,就需要從partitioner的代碼增加partitions的數量。

5. standalone模式下資源分配不均勻導致內存溢出

在standalone的模式下如果配置了–total-executor-cores 和 –executor-memory 這兩個參數,但是沒有配置–executor-cores這個參數的話,就有可能導致,每個Executor的memory是一樣的,但是cores的數量不同,那么在cores數量多的Executor中,由於能夠同時執行多個Task,就容易導致內存溢出的情況。這種情況的解決方法就是同時配置–executor-cores或者spark.executor.cores參數,確保Executor資源分配均勻。

6.在RDD中,共用對象能夠減少OOM的情況

這個比較特殊,這里說記錄一下,遇到過一種情況,類似這樣rdd.flatMap(x=>for(i <- 1 to 1000) yield (“key”,”value”))導致OOM,但是在同樣的情況下,使用rdd.flatMap(x=>for(i <- 1 to 1000) yield “key”+”value”)就不會有OOM的問題,這是因為每次(“key”,”value”)都產生一個Tuple對象,而”key”+”value”,不管多少個,都只有一個對象,指向常量池。具體測試如下:

 
 

這個例子說明(“key”,”value”)和(“key”,”value”)在內存中是存在不同位置的,也就是存了兩份,但是”key”+”value”雖然出現了兩次,但是只存了一份,在同一個地址,這用到了JVM常量池的知識.於是乎,如果RDD中有大量的重復數據,或者Array中需要存大量重復數據的時候我們都可以將重復數據轉化為String,能夠有效的減少內存使用.

6、java 實現快速排序 

高快省的排序算法

有沒有既不浪費空間又可以快一點的排序算法呢?那就是“快速排序”啦!光聽這個名字是不是就覺得很高端呢。

假設我們現在對“6 1 2 7 9 3 4 5 10 8”這個10個數進行排序。首先在這個序列中隨便找一個數作為基准數(不要被這個名詞嚇到了,就是一個用來參照的數,待會你就知道它用來做啥的了)。為了方便,就讓第一個數6作為基准數吧。接下來,需要將這個序列中所有比基准數大的數放在6的右邊,比基准數小的數放在6的左邊,類似下面這種排列:

3 1 2 5 4 6 9 7 10 8

在初始狀態下,數字6在序列的第1位。我們的目標是將6挪到序列中間的某個位置,假設這個位置是k。現在就需要尋找這個k,並且以第k位為分界點,左邊的數都小於等於6,右邊的數都大於等於6。想一想,你有辦法可以做到這點嗎?

排序算法顯神威

方法其實很簡單:分別從初始序列“6 1 2 7 9 3 4 5 10 8”兩端開始“探測”。先從找一個小於6的數,再從找一個大於6的數,然后交換他們。這里可以用兩個變量i和j,分別指向序列最左邊和最右邊。我們為這兩個變量起個好聽的名字“哨兵i”和“哨兵j”。剛開始的時候讓哨兵i指向序列的最左邊(即i=1),指向數字6。讓哨兵j指向序列的最右邊(即=10),指向數字。

 
 

首先哨兵j開始出動。因為此處設置的基准數是最左邊的數,所以需要讓哨兵j先出動,這一點非常重要(請自己想一想為什么)。哨兵j一步一步地向左挪動(即j–),直到找到一個小於6的數停下來。接下來哨兵i再一步一步向右挪動(即i++),直到找到一個數大於6的數停下來。最后哨兵j停在了數字5面前,哨兵i停在了數字7面前。

 
 
 
 

現在交換哨兵i和哨兵j所指向的元素的值。交換之后的序列如下:

6 1 2 5 9 3 4 7 10 8

 
 
 
 

到此,第一次交換結束。接下來開始哨兵j繼續向左挪動(再友情提醒,每次必須是哨兵j先出發)。他發現了4(比基准數6要小,滿足要求)之后停了下來。哨兵i也繼續向右挪動的,他發現了9(比基准數6要大,滿足要求)之后停了下來。此時再次進行交換,交換之后的序列如下:

6 1 2 5 4 3 9 7 10 8

第二次交換結束,“探測”繼續。哨兵j繼續向左挪動,他發現了3(比基准數6要小,滿足要求)之后又停了下來。哨兵i繼續向右移動,糟啦!此時哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。說明此時“探測”結束。我們將基准數6和3進行交換。交換之后的序列如下:

3 1 2 5 4 6 9 7 10 8

 
 
 
 
 
 

到此第一輪“探測”真正結束。此時以基准數6為分界點,6左邊的數都小於等於6,6右邊的數都大於等於6。回顧一下剛才的過程,其實哨兵j的使命就是要找小於基准數的數,而哨兵i的使命就是要找大於基准數的數,直到i和j碰頭為止。

OK,解釋完畢。現在基准數6已經歸位,它正好處在序列的第6位。此時我們已經將原來的序列,以6為分界點拆分成了兩個序列,左邊的序列是“3 1 2 5 4”,右邊的序列是“9 7 10 8”。接下來還需要分別處理這兩個序列。因為6左邊和右邊的序列目前都還是很混亂的。不過不要緊,我們已經掌握了方法,接下來只要模擬剛才的方法分別處理6左邊和右邊的序列即可。現在先來處理6左邊的序列現吧。

左邊的序列是“3 1 2 5 4”。請將這個序列以3為基准數進行調整,使得3左邊的數都小於等於3,3右邊的數都大於等於3。好了開始動筆吧

如果你模擬的沒有錯,調整完畢之后的序列的順序應該是:

2 1 3 5 4

OK,現在3已經歸位。接下來需要處理3左邊的序列“2 1”和右邊的序列“5 4”。對序列“2 1”以2為基准數進行調整,處理完畢之后的序列為“1 2”,到此2已經歸位。序列“1”只有一個數,也不需要進行任何處理。至此我們對序列“2 1”已全部處理完畢,得到序列是“1 2”。序列“5 4”的處理也仿照此方法,最后得到的序列如下:

1 2 3 4 5 6 9 7 10 8

對於序列“9 7 10 8”也模擬剛才的過程,直到不可拆分出新的子序列為止。最終將會得到這樣的序列,如下

1 2 3 4 5 6 7 8 9 10

到此,排序完全結束。細心的同學可能已經發現,快速排序的每一輪處理其實就是將這一輪的基准數歸位,直到所有的數都歸位為止,排序就結束了。下面上個霸氣的圖來描述下整個算法的處理過程。

 
 

這是為什么呢?

快速排序之所比較快,因為相比冒泡排序,每次交換是跳躍式的。每次排序的時候設置一個基准點,將小於等於基准點的數全部放到基准點的左邊,將大於等於基准點的數全部放到基准點的右邊。這樣在每次交換的時候就不會像冒泡排序一樣每次只能在相鄰的數之間進行交換,交換的距離就大的多了。因此總的比較和交換次數就少了,速度自然就提高了。當然在最壞的情況下,仍可能是相鄰的兩個數進行了交換。因此快速排序的最差時間復雜度和冒泡排序是一樣的都是O(N2),它的平均時間復雜度為O(NlogN)。其實快速排序是基於一種叫做“二分”的思想。我們后面還會遇到“二分”思想,到時候再聊。先上代碼,如下

代碼實現:

public class QuickSort {
    public static void quickSort(int[] arr,int low,int high){
         int i,j,temp,t;
         if(low>high){
             return;
         }
         i=low;
         j=high;
         //temp就是基准位
         temp = arr[low];
         
        while (i<j) {
             //先看右邊,依次往左遞減
             while (temp<=arr[j]&&i<j) {
                     j--;
             }
             //再看左邊,依次往右遞增
             while (temp>=arr[i]&&i<j) {
                     i++;
             }
             //如果滿足條件則交換
             if (i<j) {
                 t = arr[j];
                 arr[j] = arr[i];
                 arr[i] = t;
              }
         }
         //最后將基准為與i和j相等位置的數字交換
         arr[low] = arr[i];
         arr[i] = temp;
         //遞歸調用左半數組
         quickSort(arr, low, j-1);
         //遞歸調用右半數組
         quickSort(arr, j+1, high);
 }
    public static void main(String[] args){
         int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
         quickSort(arr, 0, arr.length-1);
         for (int i = 0; i < arr.length; i++) {
         System.out.println(arr[i]);
         }
     }
}

輸出為

1
2
2
3
4
4
7
7
8
9
10
19
62

7、設計微信群發紅包數據庫表結構(包含表名稱、字段名稱、類型) 

舉例:

drop table if exists wc_groupsend_rp;
create external table wc_groupsend_rp (
     imid string, --設備ID
     wcid string, --微信號
     wcname string, --微信名
     wcgroupName string, --群名稱
     rpamount double, --紅包金額
     rpid string, --紅包標識
     rpcount int, --紅包數量
     rptype int, --紅包類型 比如1拼手氣紅包,2為普通紅包,3為指定人領取紅包
     giverpdt string, --發紅包時間
    setuprpdt string, --創建紅包時間 點擊紅包按鈕的時間     paydt string, --支付時間
) COMMENT '群發紅包表'
PARTITIONED BY (`giverpdt` string)
row format delimited fields terminated by '\t';
 drop table if exists wc_groupcash_rp;
create external table wc_groupcash_rp (
    rpid string, --紅包標識
     imid string, --設備ID
     wcid string, --微信號
     wcname string, --微信名
    wcgroupName string, --群名稱
     cashdt stirng, --紅包領取時間 每領取一次更新一條數據 
     cashcount int, --領取人數
     cashamount double, --領取金額
     cashwcid string, --領取人的微信
     cashwcname string, --領取人微信昵稱
     cashsum double, --已領取總金額
) COMMENT '紅包領取表'
PARTITIONED BY (`rpid` string)
row format delimited fields terminated by '\t'; 

8、如何選型:業務場景、性能要求、維護和擴展性、成本、開源活躍度 

9、Spark如何調優 

1)使用foreachPartitions替代foreach。

原理類似於“使用mapPartitions替代map”,也是一次函數調用處理一個partition的所有數據,而不是一次函數調用處理一條數據。在實踐中發現,foreachPartitions類的算子,對性能的提升還是很有幫助的。比如在foreach函數中,將RDD中所有數據寫MySQL,那么如果是普通的foreach算子,就會一條數據一條數據地寫,每次函數調用可能就會創建一個數據庫連接,此時就勢必會頻繁地創建和銷毀數據庫連接,性能是非常低下;但是如果用foreachPartitions算子一次性處理一個partition的數據,那么對於每個partition,只要創建一個數據庫連接即可,然后執行批量插入操作,此時性能是比較高的。實踐中發現,對於1萬條左右的數據量寫MySQL,性能可以提升30%以上。

 

2)設置num-executors參數

參數說明:該參數用於設置Spark作業總共要用多少個Executor進程來執行。Driver在向YARN集群管理器申請資源時,YARN集群管理器會盡可能按照你的設置來在集群的各個工作節點上,啟動相應數量的Executor進程。這個參數非常之重要,如果不設置的話,默認只會給你啟動少量的Executor進程,此時你的Spark作業的運行速度是非常慢的。

 

參數調優建議:該參數設置的太少,無法充分利用集群資源;設置的太多的話,大部分隊列可能無法給予充分的資源。針對數據交換的業務場景,建議該參數設置1-5。

 

3)設置executor-memory參數

參數說明:該參數用於設置每個Executor進程的內存。Executor內存的大小,很多時候直接決定了Spark作業的性能,而且跟常見的JVM OOM異常也有直接的關聯。

 

參數調優建議:針對數據交換的業務場景,建議本參數設置在512M及以下。

 

4) executor-cores

參數說明:該參數用於設置每個Executor進程的CPU core數量。這個參數決定了每個Executor進程並行執行task線程的能力。因為每個CPU core同一時間只能執行一個task線程,因此每個Executor進程的CPU core數量越多,越能夠快速地執行完分配給自己的所有task線程。

 

參數調優建議:Executor的CPU core數量設置為2~4個較為合適。建議,如果是跟他人共享一個隊列,那么num-executors * executor-cores不要超過隊列總CPU core的1/3~1/2左右比較合適,避免影響其他人的作業運行。

 

5) driver-memory

參數說明:該參數用於設置Driver進程的內存。

 

參數調優建議:Driver的內存通常來說不設置,或者設置512M以下就夠了。唯一需要注意的一點是,如果需要使用collect算子將RDD的數據全部拉取到Driver上進行處理,那么必須確保Driver的內存足夠大,否則會出現OOM內存溢出的問題。

 

6) spark.default.parallelism

參數說明:該參數用於設置每個stage的默認task數量。這個參數極為重要,如果不設置可能會直接影響你的Spark作業性能。

 

參數調優建議:如果不設置這個參數, Spark自己根據底層HDFS的block數量來設置task的數量,默認是一個HDFS block對應一個task。Spark官網建議的設置原則是,設置該參數為num-executors * executor-cores的2~3倍較為合適,此時可以充分地利用Spark集群的資源。針對數據交換的場景,建議此參數設置為1-10。

 

7) spark.storage.memoryFraction

參數說明:該參數用於設置RDD持久化數據在Executor內存中能占的比例,默認是0.6。也就是說,默認Executor 60%的內存,可以用來保存持久化的RDD數據。根據你選擇的不同的持久化策略,如果內存不夠時,可能數據就不會持久化,或者數據會寫入磁盤。

 

參數調優建議:如果Spark作業中,有較多的RDD持久化操作,該參數的值可以適當提高一些,保證持久化的數據能夠容納在內存中。避免內存不夠緩存所有的數據,導致數據只能寫入磁盤中,降低了性能。但是如果Spark作業中的shuffle類操作比較多,而持久化操作比較少,那么這個參數的值適當降低一些比較合適。如果發現作業由於頻繁的gc導致運行緩慢(通過spark web ui可以觀察到作業的gc耗時),意味着task執行用戶代碼的內存不夠用,那么同樣建議調低這個參數的值。針對數據交換的場景,建議降低此參數值到0.2-0.4。

 

8) spark.shuffle.memoryFraction

參數說明:該參數用於設置shuffle過程中一個task拉取到上個stage的task的輸出后,進行聚合操作時能夠使用的Executor內存的比例,默認是0.2。也就是說,Executor默認只有20%的內存用來進行該操作。shuffle操作在進行聚合時,如果發現使用的內存超出了這個20%的限制,那么多余的數據就會溢寫到磁盤文件中去,此時就會極大地降低性能。

 

參數調優建議:如果Spark作業中的RDD持久化操作較少,shuffle操作較多時,建議降低持久化操作的內存占比,提高shuffle操作的內存占比比例,避免shuffle過程中數據過多時內存不夠用,必須溢寫到磁盤上,降低了性能。如果發現作業由於頻繁的gc導致運行緩慢,意味着task執行用戶代碼的內存不夠用,那么同樣建議調低這個參數的值。針對數據交換的場景,建議此值設置為0.1或以下。

 

資源參數參考示例

 

以下是一份spark-submit命令的示例,可以參考一下,並根據自己的實際情況進行調節:

./bin/spark-submit \

  --master yarn-cluster \

  --num-executors 1 \

  --executor-memory 512M \

  --executor-cores 2 \

  --driver-memory 512M \

  --conf spark.default.parallelism=2 \

  --conf spark.storage.memoryFraction=0.2 \

  --conf spark.shuffle.memoryFraction=0.1 \

10、Flink和spark的通信框架有什么異同 

 

首先它們有哪些共同點?flink和spark都是apache 軟件基金會(ASF)旗下頂級項目,都是通用數據處理平台。它們可以應用在很多的大數據應用和處理環境。並且有如下擴展:

 

 
 

並且兩者均可在不依賴於其他環境的情況下運行於standalone模式,或是運行在基於hadoop(YARN,HDFS)之上,由於它們均是運行於內存,所以他們表現的都比hadoop要好很多。

然而它們在實現上還是有很多不同點:

在spark 1.5.x之前的版本,數據集的大小不能大於機器的內存數。

Flink在進行集合的迭代轉換時可以是循環或是迭代計算處理。這使得Join算法、對分區的鏈接和重用以及排序可以選擇最優算法。當然flink也是一個很強大的批處理工具。flink的流式處理的是真正的流處理。流式數據一但進入就實時進行處理,這就允許流數據靈活地在操作窗口。它甚至可以在使用水印的流數中處理數據(It is even capable of handling late data in streams by the use of watermarks)。此外,flink的代碼執行引擎還對現有使用storm,mapreduce等有很強的兼容性。

Spark 在另一方面是基於 彈性分布式數據集(RDD),這(主要的)給於spark基於內存內數據結構的函數式編程。它可以通過固定的內存給於大批量的計算。spark streaming 把流式數據封裝成小的批處理,也就是它收集在一段時間內到達的所有數據,並在收集的數據上運行一個常規批處理程序。同時一邊收集下一個小的批處理數據。

11、Java的代理 

代理模式是什么

代理模式是一種設計模式,簡單說即是在不改變源碼的情況下,實現對目標對象功能擴展

比如有個歌手對象叫Singer,這個對象有一個唱歌方法叫sing()。

public class Singer{
    public void sing(){
        System.out.println("唱一首歌");
    }
}

假如你希望,通過你的某種方式生產出來的歌手對象,在唱歌前后還要想觀眾問好和答謝,也即對目標對象Singer的sing方法進行功能擴展。

public void sing(){
     System.out.println("向觀眾問好");
     System.out.println("唱一首歌");
     System.out.println("謝謝大家");

但是往往你又不能直接對源代碼進行修改,可能是你希望原來的對象還保持原來的樣子,又或許你提供的只是一個可插拔的插件,甚至你有可能都不知道你要對哪個目標對象進行擴展。這時就需要用到java的代理模式了。網上好多用生活中的經理人的例子來解釋“代理”,看似通俗易懂,但我覺得不適合程序員去理解。程序員應該從代碼的本質入手。

 

Java的三種代理模式

想要實現以上的需求有三種方式,這一部分我們只看三種模式的代碼怎么寫,先不涉及實現原理的部分。

1.靜態代理

 

 

public interface ISinger {
    voidsing();
}
/**
*  目標對象實現了某一接口
*/
public class Singer implements ISinger{
    public void sing(){
         System.out.println("唱一首歌");
    }
}
/**
*  代理對象和目標對象實現相同的接口
*/
public class SingerProxy implementsI Singer{
    //接收目標對象,以便調用sing方法
    private ISinger target;
    public UserDaoProxy(ISinger target){
        this.target=target;
    }
    //對目標對象的sing方法進行功能擴展
    public void sing() {
        System.out.println("向觀眾問好");
        target.sing();
        System.out.println("謝謝大家");
    }
 }

 

 

測試

 

 

/**
* 測試類
*/
public classTest {
    public static void main(String[] args) {
        //目標對象
         ISinger target =newSinger();
        //代理對象
         ISinger proxy =newSingerProxy(target);
        //執行的是代理的方法
        proxy.sing();
    }
 }

 

 

  總結:其實這里做的事情無非就是,創建一個代理類SingerProxy,繼承了ISinger接口並實現了其中的方法。只不過這種實現特意包含了目標對象的方法,正是這種特征使得看起來像是“擴展”了目標對象的方法。假使代理對象中只是簡單地對sing方法做了另一種實現而沒有包含目標對象的方法,也就不能算作代理模式了。所以這里的包含是關鍵。

缺點:這種實現方式很直觀也很簡單,但其缺點是代理對象必須提前寫出,如果接口層發生了變化,代理對象的代碼也要進行維護。如果能在運行時動態地寫出代理對象,不但減少了一大批代理類的代碼,也少了不斷維護的煩惱,不過運行時的效率必定受到影響。這種方式就是接下來的動態代理。

 

2.動態代理(也叫JDK代理)

 跟靜態代理的前提一樣,依然是對Singer對象進行擴展

 

 

public interface ISinger {
    void sing();
}
/**
*  目標對象實現了某一接口
*/
public class Singer implements ISinger{
    public void sing(){
         System.out.println("唱一首歌");
    }
}

 

 

這回直接上測試,由於java底層封裝了實現細節(之后會詳細講),所以代碼非常簡單,格式也基本上固定。

調用Proxy類的靜態方法newProxyInstance即可,該方法會返回代理類對象

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )

接收的三個參數依次為:

 ·ClassLoader loader:指定當前目標對象使用類加載器,寫法固定

·Class<?>[] interfaces:目標對象實現的接口的類型,寫法固定

·InvocationHandler h:事件處理接口,需傳入一個實現類,一般直接使用匿名內部類

測試代碼

 

 
 

 

 

總結:以上代碼只有標黃的部分是需要自己寫出,其余部分全都是固定代碼。由於java封裝了newProxyInstance這個方法的實現細節,所以使用起來才能這么方便,具體的底層原理將會在下一小節說明。

缺點:可以看出靜態代理和JDK代理有一個共同的缺點,就是目標對象必須實現一個或多個接口,加入沒有,則可以使用Cglib代理。

 

3.Cglib代理

前提條件:

需要引入cglib的jar文件,由於Spring的核心包中已經包括了Cglib功能,所以也可以直接引入spring-core-3.2.5.jar

目標類不能為final

目標對象的方法如果為final/static,那么就不會被攔截,即不會執行目標對象額外的業務方法

 

 

 

 

 

 
 

 

 
 

這里的代碼也非常固定,只有標黃部分是需要自己寫出

測試

 
 

總結:三種代理模式各有優缺點和相應的適用范圍,主要看目標對象是否實現了接口。以Spring框架所選擇的代理模式舉例

在Spring的AOP編程中:

如果加入容器的目標對象有實現接口,用JDK代理

如果目標對象沒有實現接口,用Cglib代理

12、Java的內存溢出和內存泄漏 

1.內存溢出和內存泄漏是啥

  。內存溢出 out of memory,是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是內存溢出。

  。內存泄露 memory leak,是指程序在申請內存后,無法釋放已申請的內存空間。

  是不是覺得上文中的內存泄漏的定義比較難理解?

  其實,內存泄漏用粗俗一點的話來說就是“占着茅坑不拉粑粑”。

  什么意思呢?就是說,你向系統申請分配內存進行使用(new),可是使用完了以后卻不歸還(delete),而你自己出於某些原因不能再訪問到那塊內存(也許你把它的地址給弄丟了),這時候系統也不能再次將它分配給需要的程序。

  這里需要注意一點,即一次內存泄露危害可以忽略,但內存泄露堆積后果很嚴重,無論多少內存,遲早會被占光。

  危害到底有多重呢?舉個例子,上文我們說到“占着茅坑不拉粑粑”,假設這個廁所有五個蹲位,那么一個人占用了一個且不離開,別人又拿它沒有辦法,那這個蹲位就用不了了,現在就只剩下四個。這樣似乎還可以接受,那么當三個蹲位被占用且無法被釋放的時候,剩下的兩個蹲位的壓力就很大了。當五個蹲位都被占用且無法釋放的時候,就沒有蹲位了,那些排隊等待拉粑粑的人就沒有地方宣泄自己的粑粑,這麻煩就大了。萬一人家拉褲子了咋辦呢?對吧。內存泄漏也是這樣。

  而對於溢出來說,一個盤子用盡各種方法只能裝4個果子,你裝了5個,結果掉倒地上不能吃了。這就是溢出!又比方說棧,棧滿時再做進棧必定產生空間溢出,叫上溢,棧空時再做退棧也產生空間溢出,稱為下溢。分配的內存不足以放下數據項序列,稱為內存溢出.

注:memory leak會最終會導致out of memory!

 

 

2.內存泄漏的分類

  發生的方式來分類的話,內存泄漏可以分為4類:

常發性內存泄漏:發生內存泄漏的代碼會被多次執行到,每次被執行的時候都會導致一塊內存泄漏。

偶發性內存泄漏:發生內存泄漏的代碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測內存泄漏至關重要。

一次性內存泄漏:發生內存泄漏的代碼只會被執行一次,或者由於算法上的缺陷,導致總會有一塊僅且一塊內存發生泄漏。比如,在類的構造函數中分配內存,在析構函數中卻沒有釋放該內存,所以內存泄漏只會發生一次。

隱式內存泄漏:程序在運行過程中不停的分配內存,但是直到結束的時候才釋放內存。嚴格的說這里並沒有發生內存泄漏,因為最終程序釋放了所有申請的內存。但是對於一個服務器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內存也可能導致最終耗盡系統的所有內存。所以,我們稱這類內存泄漏為隱式內存泄漏。

  從用戶使用程序的角度來看,內存泄漏本身不會產生什么危害,作為一般的用戶,根本感覺不到內存泄漏的存在。上文也有說到,真正有危害的是內存泄漏的堆積,這會最終消耗盡系統所有的內存。從這個角度來說,一次性內存泄漏並沒有什么危害,因為它不會堆積,而隱式內存泄漏危害性則非常大,因為較之於常發性和偶發性內存泄漏它更難被檢測到。所以我等程序員們在寫代碼的時候要養成好的習慣哦(在這一點上,Java的回收機制就做的很好)。

  3.內存溢出的原因

  內存溢出常見的原因有以下幾種:

  。內存中加載的數據量過於龐大,如一次從數據庫取出過多數據;

  。集合類中有對對象的引用,使用完后未清空,使得JVM不能回收;

  。代碼中存在死循環或循環產生過多重復的對象實體;

  。使用的第三方軟件中的BUG;

  。啟動參數內存值設定的過小

  會出現問題,自然也就有解決辦法了。

  4.內存溢出的解決方案:

  第一步,修改JVM啟動參數,直接增加內存。(-Xms,-Xmx參數一定不要忘記加。)

  第二步,檢查錯誤日志,查看“OutOfMemory”錯誤前是否有其它異常或錯誤。

  第三步,對代碼進行走查和分析,找出可能發生內存溢出的位置。

  第四步,使用內存查看工具動態查看內存使用情況。

  其中,第三步重點排查以下幾點:

  。檢查對數據庫查詢中,是否有一次獲得全部數據的查詢。一般來說,如果一次取十萬條記錄到內存,就可能引起內存溢出。這個問題比較隱蔽,在上線前,數據庫中數據較少,不容易出問題,上線后,數據庫中數據多了,一次查詢就有可能引起內存溢出。因此對於數據庫查詢盡量采用分頁的方式查詢。

  。檢查代碼中是否有死循環或遞歸調用。

  。檢查是否有大循環重復產生新對象實體。

  。檢查對數據庫查詢中,是否有一次獲得全部數據的查詢。一般來說,如果一次取十萬條記錄到內存,就可能引起內存溢出。這個問題比較隱蔽,在上線前,數據庫中數據較少,不容易出問題,上線后,數據庫中數據多了,一次查詢就有可能引起內存溢出。因此對於數據庫查詢盡量采用分頁的方式查詢。

  。檢查List、MAP等集合對象是否有使用完后,未清除的問題。List、MAP等集合對象會始終存有對對象的引用,使得這些對象不能被GC回收。

  好啦,這次關於內存泄漏和內存溢出的探秘就算總結完啦,當然,內存絕對不止是這些知識點,如果大家有什么別的發現,或者發現了我文中表述不對的地方,歡迎和我們一起討論學習。

13、hadoop 的組件有哪些?Yarn的調度器有哪些?

Hadoop常用組件介紹

1、Hive

Hive是將Hadoop包裝成使用簡單的軟件,用戶可以用比較熟悉的SQL語言來調取數據,也就是說,Hive其實就是將Hadoop包裝成MySQL。Hive適合使用在對實時性要求不高的結構化數據處理。像是每天、每周用戶的登錄次數、登錄時間統計;每周用戶增長比例之類的BI應用。

2、HBase

HBase是用來儲存和查詢非結構化和半結構化數據的工具,利用row key的方式來訪問數據。HBase適合處理大量的非結構化數據,例如圖片、音頻、視頻等,在訓練機器學習時,可以快速的透過標簽將相對應的數據全部調出。

3、Storm

前面兩個都是用來處理非實時的數據,對於某些講求高實時性(毫秒級)的應用,就需要使用Storm。Storm也是具有容錯和分布式計算的特性,架構為master-slave,可橫向擴充多節點進行處理,每個節點每秒可以處理上百萬條記錄。可用在金融領域的風控上。

4、Impala

Impala和Hive的相似度很高,最大的不同是Impala使用了基於MPP的SQL查詢,實時性比MapReduce好很多,但是無法像Hive一樣可以處理大量的數據。Impala提供了快速輕量查詢的功能,方便開發人員快速的查詢新產生的數據。

YRAN提供了三種調度策略

一、FIFO-先進先出調度器

    YRAN默認情況下使用的是該調度器,即所有的應用程序都是按照提交的順序來執行的,這些應用程序都放在一個隊列中,只有在前面的一個任務執行完成之后,才可以執行后面的任務,依次執行

    缺點:如果有某個任務執行時間較長的話,后面的任務都要處於等待狀態,這樣的話會造成資源的使用率不高;如果是多人共享集群資源的話,缺點更是明顯

二、capacity-scheduler-容量調度器

    針對多用戶的調度,容量調度器采用的方法稍有不同。集群由很多的隊列組成(類似於任務池),這些隊列可能是層次結構的(因此,一個隊列可能是另一個隊列的子隊列),每個隊列被分配有一定的容量。這一點於公平調度器類似,只不過在每個隊列的內部,作業根據FIFO的方式(考慮優先級)調度。本質上,容量調度器允許用戶或組織(使用隊列自行定義)為每個用戶或組織模擬出一個使用FIFO調度策略的獨立MapReduce集群。相比之下,公平調度器(實際上也支持作業池內的FIFO調度,使其類似於容量調度器)強制池內公平共享,使運行的作業共享池內的資源。

    總結:容量調度器具有以下幾個特點

    1、集群按照隊列為單位划分資源,這些隊列可能是層次結構的

    2、可以控制每個隊列的最低保障資源和最高使用限制,最高使用限制是為了防止該隊列占用過多的空閑資源導致其他的隊列資源緊張

    3、可以針對用戶設置每個用戶的資源最高使用限制,防止該用戶濫用資源

    4、在每個隊列內部的作業調度是按照FIFO的方式調度的

    5、如果某個隊列的資源使用緊張,但是另一個隊列的資源比較空閑,此時可以將空閑的資源暫時借用,但是一旦被借用資源的隊列有新的任務提交之后,此時被借用出去的資源將會被釋放還回給原隊列

    6、每一個隊列都有嚴格的訪問控制,只有那些被授權了的用戶才可以查看任務的運行狀態。

   配置文件的說明(capacity-scheduler.xml):

 

 
 

 

 
 
 
 

三、Fair-scheduler-公平調度器

    所謂的公平調度器指的是,旨在讓每個用戶公平的共享集群的能力。如果是只有一個作業在運行的話,就會得到集群中所有的資源。隨着提交的作業越來越多,限制的任務槽會以“讓每個用戶公平共享集群”這種方式進行分配。某個用戶的好事短的作業將在合理的時間內完成,即便另一個用戶的長時間作業正在運行而且還在運行過程中。

    作業都是放在作業池中的,默認情況下,每個用戶都有自己的作業池。提交作業數較多的用戶,不會因此而獲得更多的集群資源。可以用map和reduce的任務槽數來定制作業池的最小容量,也可以設置每個池的權重。

    公平調度器支持搶占機制。所以,如果一個池在特定的一段時間內未能公平的共享資源,就會終止運行池中得到過多的資源的任務,把空出來的任務槽讓給運行資源不足的作業池。

    主要特點:

      1、也是將集群的資源以隊列為單位進行划分,稱為隊列池

      2、每個用戶都有自己的隊列池,如果該隊列池中只有一個任務的話,則該任務會使用該池中的所有資源

      3、每個用戶提交作業都是提交到自己的隊列池中,所以,提交作業數較多的用戶,並不會因此而獲得更多的集群資源

      4、支持搶占機制。也就是說如果一個吃在特定的時間內未能公平的共享資源,就會終止池中占用過多資源的任務,將空出來的任務槽讓給運行資源不足的作業池。

      5、負載均衡:提供一個基於任務數目的負載均衡機制。該機制盡可能的將任務均勻的分配到集群的所有的節點上。

14、hadoop 的 shuffle 過程 

 
 

流程解釋:

 
 

以wordcount為例,假設有5個map和3個reduce:

map階段

1、在map task執行時,它的輸入數據來源於HDFS的block,當然在MapReduce概念中,map task只讀取split。Split與block的對應關系可能是多對一,默認是一對一。

2、在經過mapper的運行后,我們得知mapper的輸出是這樣一個key/value對: key是“hello”, value是數值1。因為當前map端只做加1的操作,在reduce task里才去合並結果集。這個job有3個reduce task,到底當前的“hello”應該交由哪個reduce去做呢,是需要現在決定的。

分區(partition)

MapReduce提供Partitioner接口,它的作用就是根據key或value及reduce的數量來決定當前的這對輸出數據最終應該交由哪個reduce task處理。默認對key hash后再以reduce task數量取模。默認的取模方式只是為了平均reduce的處理能力,如果用戶自己對Partitioner有需求,可以訂制並設置到job上。

一個split被分成了3個partition。

排序sort

在spill寫入之前,會先進行二次排序,**首先根據數據所屬的partition進行排序,然后每個partition中的數據再按key來排序。**partition的目是將記錄划分到不同的Reducer上去,以期望能夠達到負載均衡,以后的Reducer就會根據partition來讀取自己對應的數據。接着運行combiner(如果設置了的話),combiner的本質也是一個Reducer,其目的是對將要寫入到磁盤上的文件先進行一次處理,這樣,寫入到磁盤的數據量就會減少。

溢寫(spill)

Map端會處理輸入數據並產生中間結果,這個中間結果會寫到本地磁盤,而不是HDFS。每個Map的輸出會先寫到內存緩沖區中, 緩沖區的作用是批量收集map結果,減少磁盤IO的影響。我們的key/value對以及Partition的結果都會被寫入緩沖區。當然寫入之前,key與value值都會被序列化成字節數組。 當寫入的數據達到設定的閾值時,系統將會啟動一個線程將緩沖區的數據寫到磁盤,這個過程叫做spill。

這個溢寫是由單獨線程來完成,不影響往緩沖區寫map結果的線程。溢寫線程啟動時不應該阻止map的結果輸出,所以整個緩沖區有個溢寫的比例spill.percent。這個比例默認是0.8。

將數據寫到本地磁盤產生spill文件(spill文件保存在{mapred.local.dir}指定的目錄中,MapReduce任務結束后就會被刪除)。

合並(merge)

每個Map任務可能產生多個spill文件,在每個Map任務完成前,會通過多路歸並算法將這些spill文件歸並成一個文件。這個操作就叫merge(spill文件保存在{mapred.local.dir}指定的目錄中,Map任務結束后就會被刪除)。一個map最終會溢寫一個文件。

至此,Map的shuffle過程就結束了。

Reduce階段

Reduce端的shuffle主要包括三個階段,copy、sort(merge)和reduce。

copy

首先要將Map端產生的輸出文件拷貝到Reduce端,但每個Reducer如何知道自己應該處理哪些數據呢?因為Map端進行partition的時候,實際上就相當於指定了每個Reducer要處理的數據(partition就對應了Reducer),**所以Reducer在拷貝數據的時候只需拷貝與自己對應的partition中的數據即可。**每個Reducer會處理一個或者多個partition,但需要先將自己對應的partition中的數據從每個Map的輸出結果中拷貝過來。

merge

Copy過來的數據會先放入內存緩沖區中,這里的緩沖區大小要比map端的更為靈活,它基於JVM的heap size設置,因為Shuffle階段Reducer不運行,所以應該把絕大部分的內存都給Shuffle用。

這里需要強調的是:

merge階段,也稱為sort階段,因為這個階段的主要工作是執行了歸並排序。從Map端拷貝到Reduce端的數據都是有序的,所以很適合歸並排序。

merge有三種形式:1)內存到內存 2)內存到磁盤 3)磁盤到磁盤。默認情況下第一種形式不啟用,讓人比較困惑,是吧。

當copy到內存中的數據量到達一定閾值,就啟動內存到磁盤的merge,即第二種merge方式,與map 端類似,這也是溢寫的過程,這個過程中如果你設置有Combiner,也是會啟用的,然后在磁盤中生成了眾多的溢寫文件。這種merge方式一直在運行,直到沒有map端的數據時才結束。

然后啟動第三種磁盤到磁盤的merge方式生成最終的那個文件。

reduce

不斷地merge后,最后會生成一個“最終文件”。為什么加引號?因為這個文件可能存在於磁盤上,也可能存在於內存中。對我們來說,當然希望它存放於內存中,直接作為Reducer的輸入,但默認情況下,這個文件是存放於磁盤中的。至於怎樣才能讓這個文件出現在內存中,參見性能優化篇。

然后就是Reducer執行,在這個過程中產生了最終的輸出結果,並將其寫到HDFS上。

15、簡述Spark集群運行的幾種模式 

    a.local本地模式

    b.Spark內置standalone集群模式

    c.Yarn集群模式

16、RDD 中的 reducebyKey 與 groupByKey 哪個性能高?

17、簡述 HBase 的讀寫過程 

18、在 2.5億個整數中,找出不重復的整數,注意:內存不足以容納 2.5億個整數。

19、CDH 和 HDP 的區別 

20、Java原子操作 

21、Java封裝、繼承和多態 

22、JVM 模型 

23、Flume taildirSorce 重復讀取數據解決方法 

24、Flume 如何保證數據不丟 

25、Java 類加載過程 

26、Spark Task 運行原理 

27、手寫一個線程安全的單例 

28、設計模式 

29、impala 和 kudu 的適用場景,讀寫性能如何 

30、Kafka ack原理 

31、phoenix 創建索引的方式及區別 

32、Flink TaskManager 和 Job Manager 通信 

33、Flink 雙流 join方式 

34、Flink state 管理和 checkpoint 的流程 

35、Flink 分層架構 

36、Flink 窗口 

37、Flink watermark 如何處理亂序數據 

38、Flink time 

39、Flink支持exactly-once 的 sink 和 source 

40、Flink 提交作業的流程 

41、Flink connect 和 join 區別 

42、重啟 task 的策略 

43、hive 的鎖 

44、hive sql 優化方式 

45、hadoop shuffle 過程和架構 

46、如何優化 shuffle過程 

47、冒泡排序和快速排序 

48、講講Spark的stage 

49、spark mkrdd和Parrallilaze函數區別 

50、Spark checkpoint 過程 

51、二次排序 

52、如何注冊 hive udf 

53、SQL去重方法 

54、Hive分析和窗口函數 

55、Hadoop 容錯,一個節點掛掉然后又上線 

56、掌握 JVM 原理 

57、Java 並發原理 

58、多線程的實現方法 

59、RocksDBStatebackend實現(源碼級別) 

60、HashMap、ConcurrentMap和 Hashtable 區別 

61、Flink Checkpoint 是怎么做的,作用到算子還是chain 

62、Checkpoint失敗了的監控 

63、String、StringBuffer和 StringBuilder的區別 

64、Kafka存儲流程,為什么高吞吐?

65、Spark優化方法舉例 

66、keyby的最大並行度 

67、Flink 優化方法 

68、Kafka ISR 機制 

69、Kafka partition的4個狀態 

70、Kafka 副本的7個狀態 

71、Flink taskmanager的數量 

72、if 和 switch 的性能及 switch 支持的參數 

73、kafka 零拷貝 

74、hadoop 節點容錯機制 

75、HDFS 的副本分布策略 

76、Hadoop面試題匯總,大概都在這里(https://www.cnblogs.com/gala1021/p/8552850.html) 

77、Kudu 和Impala 權限控制 

78、Time_wait狀態?當server處理完client的請求后立刻closesocket此時會出現time_wait狀態

79、三次握手交換了什么?(SYN,ACK,SEQ,窗口大小) 3次握手建立鏈接,4次握手斷開鏈接。

80、hashmap 1.7和1.8 的區別 

81、concurrenthashmap 1.7和1.8?

82、Kafka 的ack 

83、sql 去重方法(group by 、distinct、窗口函數) 

84、哪些 Hive sql 不能在 Spark sql 上運行,看這里:https://spark.apache.org/docs/2.2.0/sql-programming-guide.html#unsupported-hive-functionality 

85、什么情況下發生死鎖 

86、事務隔離級別?可重復讀、不可重復讀、讀未提交、串行化 

87、Spark shuffle 和 Hadoop shuffle的異同 

88、Spark靜態內存和動態內存 

89、mysql btree 和 hash tree 的區別。btree 需要唯一主鍵,hash tree 適合>= 等,精確匹配,不適合范圍檢索 

90、udf、udtf和 udaf 的區別 

91、hive sql 的執行過程 

92、spark sql 的執行過程 

93、找出數組中最長的top10字符串 

94、Flink 數據處理流程 

95、Flink 與 Spark streaming 對比 

96、Flink watermark 使用 

97、窗口與流的結合 

98、Flink 實時告警設計 

99、Java:面向對象、容器、多線程、單例 

100、Flink:部署、API、狀態、checkpoint、savepoint、watermark、重啟策略、datastream 算子和優化、job和task狀態 

101、Spark:原理、部署、優化 

102、Kafka:讀寫原理、使用、優化 

103、hive的外部表 

104、spark的函數式編程 

105、線性數據結構和數據結構 

106、Spark映射,RDD

107、java的內存溢出和內存泄漏

108、多線程的實現方法 

109、HashMap、ConcurrentMap和 Hashtable 區別 

110、Flink Checkpoint 是怎么做的,作用到算子還是chain 

111、Checkpoint失敗了的監控 

112、String、StringBuffer和 StringBuilder的區別 

113、Kafka存儲流程,為什么高吞吐 

114、Spark 優化方法舉例 

115、keyby 的最大並行度 

116、Flink 優化方法 

117、Kafka ISR 機制 

118、kafka partition 的狀態 

119、kafka 副本的狀態 

120、taskmanager 的數量 

121、if 和switch的性能區別

122、Hdfs讀寫流程(結合cap理論講) 

123、技術選型原則 

124、Kafka組件介紹 

125、g1和cms的區別 

126、講講最熟悉的數據結構 

127、spark oom處理方法 

128、看了哪些源碼 

129、Spark task原理 

130、解決過的最有挑戰的問題 

131、Hbase讀寫流程



作者:一枚柚耳
鏈接:https://www.jianshu.com/p/b48f3ae30f23
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM