分類
發燒車訊

LeetCode 75,90%的人想不出最佳解的簡單題

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是LeetCode專題的44篇文章,我們一起來看下LeetCode的75題,顏色排序 Sort Colors。

這題的官方難度是Medium,通過率是45%,點贊2955,反對209(國際版數據),從這份數據上我們大概能看得出來,這題的難度不大,並且點贊遠遠高於反對,說明題目的質量很不錯。事實上也的確如此,這題足夠簡單也足夠有趣,值得一做。

題意

給定一個n個元素的數組,數組當中的每一個元素表示一個顏色。一共有紅白藍三種顏色,分別用0,1和2來表示。要求將這些顏色按照大小進行排序,返回排序之後的結果。

要求不能調用排序庫sort來解決問題。

桶排序

看完題目應該感受到了,如果沒有不能使用sort的限制,這題毫無難度。即使加上了限制難度也不大,我們既然不能調用sort,難道還不能自己寫個sort嗎?Python寫個快排也才幾行而已。

自己寫sort當然是可以的,顯然這是下下策。因為元素只有3個值,互相之間的大小關係也就只有那麼幾種,排序完全沒有必要。比較容易想到,我們可以統計一下這三個數值出現的次數,幾個0幾個1幾個2,我們再把這些數拼在一起,還原之前的數據不就可以了嗎?

這樣的確可行,但實際上這也是一種排序方案,叫做基數排序,也稱為桶排序,還有些地方稱為小學生排序(大概是小學生都能懂的意思吧)。基數排序的思想非常簡單,我們創建一個數組,用它的每一位來表示某個元素是否在原數組當中出現過。出現過則+1,沒出現過則一直是0。我們標記完原數組之後,再遍歷一遍標記的數組,由於下標天然有序,所以我們就可以得到排序之後的結果了。

如果你還有些迷糊也沒有關係,我們把代碼寫出來就明白了,由於這題讓我們提供一個inplace的方法,所以我們在最後的時候需要對nums當中的元素重新賦值。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """  Do not return anything, modify nums in-place instead.  """
        bucket = [0 for _ in range(3)]
        for i in nums:
            bucket[i] += 1

        ret = []
        for i in range(3):
            ret += [i] * bucket[i]

        nums[:] = ret[:]

和排序相比,我們只是遍歷了兩次數據,第一次是遍歷了原數組獲得了其中0,1和2的數量,第二次是將獲得的數據重新填充回原數組當中。相比於快排或者是其他一些排序算法的耗時,桶排序只遍歷了兩次數組,明顯要快得多。但遺憾的是這並不是最佳的方法,題目當中明確說了,還有隻需要遍歷一次原數組的方法。

two pointers

在我們介紹具體的算法之前,我們先來分析一下問題。既然顏色只有三種,那麼當我們排完序之後,整個數組會被分成三個部分,頭部是0,中間是1,尾部是2。

我們可以用一個區間來收縮1的範圍,假設我們當前區間的首尾元素分別是l和r。當我們讀到0的時候,我們就將它和l交換,然後將l向後移動一位。當我們讀到2的時候,則將它和r進行交換,將r向左移動一位。也就是說我們保證l和r之間的元素只有1。

我們之前曾經介紹過這種維護一個區間的做法,雖然都是維護了一個區間,但是操作上是有一些區別的。之前介紹的two pointers算法,也叫做尺取法,本質上是通過移動區間的右側邊界來容納新的元素,通過移動左邊界彈出數據的方式來維護區間內所有元素的合法性。而當前的做法中,一開始獲得的就是一個非法的區間,我們通過元素的遍歷以及區間的移動,最後讓它變得合法。兩者的思路上有一些細微的差別,但形式是一樣的,就是通過移動左右兩側的邊界來維護或者是達到合法。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """  Do not return anything, modify nums in-place instead.  """
        l, r = 0, len(nums)-1
        i = 0
        while i < len(nums):
            if i > r:
                break
   # 如果遇到0,則和左邊交換
            if nums[i] == 0:
                nums[l], nums[i] = nums[i], nums[l]
                l += 1
            # 如果遇到2,則和右邊交換
            # 交換之後i需要-1,因為可能換來一個0
            elif nums[i] == 2:
                nums[r], nums[i] = nums[i], nums[r]
                r -= 1
                continue
            i += 1

這種方法我們雖然只遍歷了數組一次,但是由於交換的次數過多,整體運行的速度比上面的方法還要慢。所以遍歷兩次數組並不一定就比只遍歷一次要差,畢竟兩者都是的算法,相差的只是一個常數。遍歷的次數只是構成常數的部分之一。

除了這個方法之外,我們還有其他維護區間的方法。

維護區間

接下來要說的方法非常巧妙,我個人覺得甚至要比上面的方法還有巧妙。

我們來假想一下這麼一個場景,假設我們不是在原數組上操作數據,而是從其中讀出數據放到新的數組當中。我們先不去想應該怎麼擺放這個問題,我們就來假設我們原數組當中的數據已經放好了若干個,那麼這個時候的新數組會是什麼樣?顯然,應該是排好序的,前面若干個0,中間若干個1,最後若干個2。

那麼問題來了,假設這個時候我們讀到一個0,那麼應該怎麼放呢?為了簡化敘述我們把它畫成圖:

我們假設藍色部分是0,綠色部分是1,粉色部分是2。a是0最右側的下標,b是1部分最右側的下標,c是2部分最右側的下標。那麼這個時候,當我們需要放入一個0的時候,應該怎麼辦?

我們結合圖很容易想明白,我們需要把0放在a+1的位置,那麼我們需要把後面1和2的部分都往右側移動一格,讓出一格位置出來放0。我們移動數組顯然帶來的開銷會過於大,實際上沒有必要移動整個部分,只需要移動頭尾元素即可。比如1的部分左側被0佔掉了一格,那麼為了保持長度不變,右側也需要延伸一格。同理,2的部分右側也需要延伸一格。那麼整個操作用代碼來表示就是:nums[a+1] = 0,nums[b+1] = 1, nums[c+1] = 2。

假設我們讀入的數是1,那麼我們需要把b延長一個單位,但是這樣帶來的後果是2的部分被侵佔,所以需要將2也延長,補上被1侵佔的一個單位。如果讀到的是2,那麼直接延長2即可,因為2後面沒有其他顏色了。

假設我們有一個空白的數組,我們可以這麼操作,但其實我們沒有必要專門創建一個數組,我們完全可以用原數組自己填充自己。因為我們從原數組上讀取的數和擺放的數是一樣的,我們直接把数字擺放在原數組的頭部,佔用之前讀取的數即可。

光說可能還有些迷糊,看下代碼馬上就清楚了:

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """  Do not return anything, modify nums in-place instead.  """
        # 記錄0,1和2的末尾位置
        zero, one, two = -1, -1, -1
        n = len(nums)
        for i in range(n):
            # 如果擺放0
            # 那麼1和2都往後平移一位,讓一個位置出來擺放0
            if nums[i] == 0:
                nums[two+1] = 2
                nums[one+1] = 1
                nums[zero+1] = 0
                zero += 1
                one += 1
                two += 1
            elif nums[i] == 1:
                nums[two+1] = 2
                nums[one+1] = 1
                one += 1
                two += 1
            else:
                nums[two+1] = 2
                two += 1

總結

到這裏,這道題的解法基本上都講完了。

相信大家也都看出來了,從難度上來說這題真的不難,相信大家都能想出解法來,但是要想到最優解還是有些困難的。一方面需要我們對題目有非常深入的理解,一方面也需要大量的思考。這類題目沒有固定的解法,需要我們根據題目的要求以及實際情況自行設計解法,這也是最考驗思維能力以及算法設計能力的問題,比考察某個算法會不會的問題要有意思得多。

希望大家都能從這題當中獲得樂趣,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

Action的三種實現方式,struts.xml配置的詳細解釋及其簡單執行過程(二)

勿以惡小而為之,勿以善小而不為————————–劉備

勸諸君,多行善事積福報,莫作惡

上一章簡單介紹了Struts2的’兩個蝴蝶飛,你好’ (一),如果沒有看過,請觀看上一章

一 Action的三種實現方式

上一章開發的HelloAction和HelloAction2,並沒有繼承任何類或者實現任何接口,但是必須有一個execute() 方法,方法返回值是String類型。

這樣的代碼不容易理解,更並不能使人看得出這個類是干什麼的,甚至不能區分這個控制器類與普通的Java類有什麼區別,通常開發中不這樣做。

我們開發者在開發Struts2框架的時候,希望自己寫的這個Action類能夠具有易理解性,且已經支持某些功能,如參數接收,文件上傳等。

一.一 第一種實現方式(普通Java類,裏面只包含execute()方法)

package com.yjl.web.action;
import org.apache.log4j.Logger;
/**
* @author 兩個蝴蝶飛
* @version 創建時間:2018年8月23日 上午9:41:32
* @description 第一種實現方式,普通java類,
* 有一個execute()方法,也可以多寫幾個方法,用action中的標籤method來控制,可以正常訪問。
*/
public class Hello1Action {
	private static Logger logger=Logger.getLogger(Hello1Action.class);
	public String execute() {
		logger.info("兩個蝴蝶飛,web層你好");
		return "success";
	}
}

不具有開發時要求的規範性,且不支持某些struts2自身提供的功能。

方法名稱只有一個 execute()

一.二 第二種實現方式(實現Action接口)

package com.yjl.web.action;
import com.opensymphony.xwork2.Action;
/**
* @author 兩個蝴蝶飛
* @version 創建時間:2018年8月23日 上午10:54:03
* @description 第二種實現方式,實現Action接口,重寫裏面的execute()方法
* 有一個execute()方法和五個String類型的常量
*/
public class Hello2Action implements Action{
	@Override
	public String execute() throws Exception {
		return Action.SUCCESS;
		//return Action.ERROR;
		//return Action.LOGIN;
		//return Action.NONE;
		//return Action.INPUT;
	}
}

注意,Action接口是xwork2包下的接口。

實現了Action接口,使開發者能夠看出來這是一個Action,具有了一定程度上的開發規範,

但是實現了Action接口,所以必須要重寫execute()方法。

一般自己寫Action,構思好之後上來就直接add(), edit(), delete(). select() 這些業務方法,

每次都要重寫execute()方法,不太方便。 而且這種方式不具有struts2中某些功能,如驗證框架和國際化。

Action中接口中有五個常用的結果字符串(好多方法都返回success,error,login,input,none,故將其封裝了一下) .

這些字符串雖然是大寫,然而真實的值是全部小寫.

package com.opensymphony.xwork2;

public abstract interface Action
{
  public static final String SUCCESS = "success";
  public static final String NONE = "none";
  public static final String ERROR = "error";
  public static final String INPUT = "input";
  public static final String LOGIN = "login";
  
  public abstract String execute()
    throws Exception;
}

一.三 繼承ActionSupport類(官方推薦)

	package com.yjl.web.action;
	import com.opensymphony.xwork2.ActionSupport;
	/**
	* @author 兩個蝴蝶飛
	* @version 創建時間:2018年8月23日 上午11:04:20
	* @description 第三種方式,繼承ActionSupport類。
	* ActionSupport類實現了Action接口,也有Action中的五個常量.
	*/
	public class Hello3Action extends ActionSupport{
		public String list() {
			return "list";
		}
	}

繼承了ActionSupport類,不需要重新寫execute()方法,直接寫業務方法即可。

ActionSupport類,已經實現了 Action接口。 其具備Action中的五個常量,並且該類還實現了其他接口,

源代碼:

	public class ActionSupport implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider, Serializable{
    ...
	public String execute() throws Exception
  	{
		//默認返回的是 success 字符串 
  		 return "success";
 	}
 	...
}

如驗證框架(Validateable,ValidationAware),國際化(LocaleProvider)。

以後開發中,使用 繼承 ActionSupport 類的形式。

二 配置文件 struts.xml中節點的詳細解釋

在src下有一個struts.xml的配置文件,它配置了開發者自己編寫實現的Action,是struts2框架的核心,不能改變文件名稱。(注意,是struts.xml,並不是struts2.xml,並沒有那個2)。

在struts.xml中,最上面是一個約束, 是一個根節點。

二.一 修改常量節點

在struts-core.jar核心包下,有一個包org.apache.struts2包下,有一個default.properties屬性文件,裏面記錄了很多常用的常量,

其中常見的有:

struts.i18n.encoding=UTF-8 
struts.multipart.maxSize=2097152
struts.action.extension=action,,
struts.enable.DynamicMethodInvocation = false
struts.devMode = false
struts.ui.theme=xhtml
struts.ognl.allowStaticMethodAccess=false

建議修改后的值為:

###國際化操作,編碼格式為UTF-8
struts.i18n.encoding=UTF-8
###上傳文件時最大的上傳大小,默認為2M. 根據項目情況具體填寫值,建議後面加兩個00
struts.multipart.maxSize=209715200
###struts的訪問後綴名, struts1框架默認的是 .do 
struts.action.extension=action,,
###struts是否可以訪問靜態方法
struts.enable.DynamicMethodInvocation =true
###struts是否是開發者模式
struts.devMode =true
###struts中ui標籤的主題,建議為simple
struts.ui.theme=simple
###ognl中是否可以訪問靜態方法,為true
struts.ognl.allowStaticMethodAccess=true

可以在struts.xml中進行相應的修改,如

 <!--修改國際化編碼 -->
<constant name="struts.i18n.encoding" value="UTF-8"></constant>
<!--修改是否為開發者模式 -->
<constant name="struts.devMode" value="true"></constant>

按照name,value值的形式進行填寫。

也可以在src下新建一個struts.properties,然後將這些值放置進去,struts也會自動struts.propeties中的常量值的。

也可以在web.xml中,在 中,以 局部參數的形式傳遞進去。

建議使用第一種方式,在struts.xml中用 ,畢竟這個文件常常打開,出錯了也容易發現。

二.二 分模塊開發

在實際的項目中,有很多的模塊,如果所有的配置都放在一個struts.xml,那麼一旦這個struts.xml被其他人誤操作導致了錯誤,那麼其他人的項目將無法運行的,當配置內容過多時,struts.xml的內容太長,不便於維護,所以最好是分模塊開發,一個模塊用一個配置文件,然後再利用 進行導入, 類似 於jsp中的 靜態包含一樣。

所以建議每一個模塊都寫一個模塊.xml,然後在struts.xml中引入即可。如有三個模塊 User模塊和Class,Course,那麼可以將User的配置放置在user.xml中,Class配置放置在class.xml中,course模塊放置在course.xml,在struts.xml中只需要

	<include file="user.xml"></include>
	<include file="class.xml"></include>
	<include file="course.xml"></include>

靜態包含即可。 注意,file的文件路徑引用是否輸入正確。

正確的位置引用,點擊ctrl+模塊.xml時,可以跳轉到相應的.xml文件中。如果沒有跳轉和反應,那說明位置引用錯誤,需要重新檢查一下。

二.三 包節點

在struts.xml配置文件中,最重要的節點就是package節點。 package,分包。 可以將action進行分包處理。

這樣每一個action或者每一組action用package進行隔開,便於維護,類似於java中package的概念。

二.三.一 <package> 節點的使用

<package name="hello" extends="struts-default" namespace="/">
        <!--具體的Action-->
</package>

package中name節點是package的名字,是獨一無二的,不能夠重複。 最好與模塊名相同或者起一個有意義的名稱。

extends節點表示繼承,即package之間可以相互的繼承,來避免重複化功能的編寫。 默認為struts-default。

struts-default中struts已經定義了很多功能,開發者自己寫的包只需要extends 這個包名struts-default,

就擁有了struts已經定義好的功能。 如攔截器功能,文件上傳功能。

用戶也可以自己繼承自己所寫的包 。如父包名為

那麼子包只需要 , 這樣child包不但擁有struts-default的功能,也擁有parent包中的特殊功能,這也是Java的多重繼承的體現。 所以package的name 要符合標識符的規範,具有可讀性。

namespace節點表示命名空間,以/開頭,默認是”/” 。是為了在訪問路徑和訪問請求url方面體現package的分包作用. package中的name是在配置文件中體現分包,namespace是在url中體現分包。 建議開發中,namespace的路徑名與name保持一致。 package中的namespace的值與子節點action中name的值,共同構成了完整的訪問請求路徑。

二.三.二 <package></package> 子節點<action></action>節點的使用

在Hello3Action中定義兩個方法,一個是list()查詢,一個是add()添加的方法。

package com.yjl.web.action;
import org.apache.log4j.Logger;
import com.opensymphony.xwork2.ActionSupport;
/**
* @author 兩個蝴蝶飛
* @version 創建時間:2018年8月23日 上午11:04:20
* @description 測試action標籤中method的方法訪問
*/
public class Hello3Action extends ActionSupport{
	private static final long serialVersionUID = 8737138848863458260L;
	Logger logger=Logger.getLogger(Hello3Action.class);
	public String list() {
		logger.info("執行list方法");
		return "list";
	}
	public String add() {
		logger.info("執行add方法");
		return "add";
	}
}

標籤,有三個基本的屬性,

	<action name="list" class="com.yjl.web.action.Hello3Action"
        method="list">

</action>

其中name為action的名字,表示區別一個package包下的不同的action。 其中這個name的值,不應該隨便取,應該是要訪問的方法名。

在瀏覽器客戶端請求的url為 /項目名/package的namespace名稱/action的name名稱.action;

class為要訪問的那個Action的全限定名稱,是class,用.(點)進行分隔。

其中,class 可以省略, 省略默認為 ActionSupport 類, 全限定名稱為: com.opensymphony.xwork2.ActionSupport
method為要訪問的那個方法名稱,類 extends ActionSupport 后,有很多很多的方法,如list(), add(), delete()等,那麼怎麼知道具體要訪問哪個方法呢? 用method這個屬性. method=”要方法的方法名” ,是方法名。

action還有一個節點是converter,表示所用的是哪一個類型轉換器。(後面會有相應的解釋)

很清楚, action 中的 class指定了訪問的是哪一個action, method 指定了訪問的是哪一個具體的方法, 利用了反射技術實現。

在本實例了有兩個方法,所以要進行寫兩個Action, 一個Action類中會有多個方法,難道要一個個配置多個Action嗎?

Struts2提供了一些簡單的方式

二.三.三 配置Action的三種形式

二.三.三.一 通過配置method的屬性完成

簡單舉例如下:

	<action name="list" class="com.yjl.web.action.Hello3Action"
		method="list">
			
	</action>
  <action name="add" class="com.yjl.web.action.Hello3Action"
		method="add">
            
    </action>

缺點: 有幾個方法,就要配置有幾個action,當方法過多時,不易維護。

二.三.三.二 通過配置 通配符完成。

簡單舉例如下:

	<action name="Hello3_*" class="com.yjl.web.action.Hello3Action"
		method="{1}">
			
		</action>

name的值為: 類簡寫名(去掉Action后)_* method中的值取第一個{1},從1開始,不是從0開始。

這樣訪問Hello3Action中的list方法,訪問路徑就是 Hello3_list

訪問Hello3Action中的add方法,訪問路徑就是Hello3_add

簡化了action的相關配置。

也有的人配置的更狠, 會配置成_, 即:

	<action name="*_*" class="com.yjl.web.action.{1}Action"
		method="{2}">
			
		</action>

User類中的list就是User_list, User類中的add就是User_add,

Class類中的list就是Class_list,Class類中的add就是Class_add

這樣雖說簡化了開發,但卻不利用 result 節點的維護 ,不建議這樣配置。

好多類的好多方法返回值,都寫在這一個action 下面,會亂。

二.三.三.三 動態方法訪問

不是用 * 通配符,而是用! 號。 即:

想訪問UserAction中list方法() 前端寫url為 userAction!list.action
想訪問UserAction中add方法() 前端寫url為 userAction!add.action
想訪問ClassAction中list方法() 前端寫url為 classAction!list.action
想訪問ClassAction中add方法() 前端寫url為 classAction!add.action

這樣訪問也特別的方便。

這樣的話, action中只需要配置name和class即可。 method已經由外部指定了,不需要寫method的值了。

需要先添加變量 struts.enable.DynamicMethodInvocation, 使其變成 true,開啟。

	<constant name="struts.enable.DynamicMethodInvocation" value="true"></constant>

如果是UserAction的話,配置應該是:

<action name="userAction" class="com.yjl.web.action.UserAction" >
			
</action>

ClassAction的話,配置應該是

<action name="classAction" class="com.yjl.web.action.ClassAction" >
			
</action>

二.三.四 action子節點result的配置

result表示結果,是對方法的返回值進行相應的分析。有兩個屬性,name和type

	<result name="success" type="dispatcher">/index.jsp</result>

其中name的值要與方法的返回值保持一致。

如 list方法返回值是return SUCCESS,那麼這個list方法的返回值對應的result的值就是 ,

如果返回是”hello”, 那麼這個name的返回值就是

如果在action中配置通配符, name=Hello3_*形式,method=”{1}”, 那麼為了簡化result的配置,可以將result配置成 name={1},

相應的.jsp,可以變成 /{1}.jsp。

但這樣必須保證Action中方法的名稱與返回值的名稱相同,並且與跳轉到的jsp的名稱也要相同, 這樣不太好。

result中type五種常見的形式, dispatcher(轉發到jsp),redirect(重定向到jsp), chain(轉發到另外一個方法),redirectAction(重定向到另外一個方法),stream(上傳和下載流)

其中dispathcer和redirect是跳轉到jsp,如果想要傳遞數據,用dispather,

如果不想傳遞數據,用redirect (dispathcer是轉發,redirect是重定向)

chain,redirectAction是跳轉到action的操作,一般用於這同一個類中兩個方法之間的跳轉,

如add()添加成功之後,需要跳轉到list()方法進行显示結果,這時就可以配置成:

	<result name="add" type="redirectAction">Hello3_list</result>

地址url也會相應的改變,如果是chain的話,地址欄是不會改變的。 chain是轉發到action, redirectAction是重定向到action.

也可以在不同包之間的action進行的跳轉 。

如 add 方法 想到跳轉到 /class 命名空間下的 Hello2Action 的 list 方法。

<result name="add" type="redirectAction">
	<!-- 要跳轉到哪一個命名空間,即哪一個包 -->
	<param name="namespace">/class</param>
	<!-- 要跳轉到哪一個Action 不加後綴 -->
	<param name="actionName">Hello2Action</param>
	<!-- 跳轉到哪一個方法 -->
	<param name="method">list</param>
	<!-- 可能要傳遞的參數. 用ognl表達式,根據情況添加 -->
	<param name="id">${id}</param>
</result>

通過 param 標籤來配置帶參還是不帶參。

二.四 全局結果頁面與局部結果頁面。

這個全局是相對於package來說的,是package中的全局,並不是所有的struts.xml中的全局,所以全局結果的節點位置應該放在package節點裏面,與action節點平行。 用 節點。

常用的全局結果頁面有兩種:

error錯誤頁面,頁面出錯了都显示這個頁面,

login 登錄頁面, 如果沒有登錄,輸入任何url都會跳轉到login頁面(認證時用)

noprivilege 沒有權限頁面,如果用戶沒有權限訪問了某一個頁面,會給出相應的提示(授權時用)

<global-results>
			<result name="error">/error/error.jsp</result>
			<result name="login">/login.jsp</result>
            <result name="noprivilege">/noprivilege.jsp</result>
</global-results>

當全局結果頁面與局部結果頁面發生衝突時,以局部結果頁面為準。

全局配置時:

<global-results>
			<result name="success">/successGlobal.jsp</result>
</global-results>

在該包下的某個action 的方法result 也返回了 success

	<result name='success'>success.jsp</result>

那麼,當邏輯視圖為 success時,最終將返回 success.jsp

二.五 配置跳轉頁面

在開發中,常常有這麼一種情況,

請求login.jsp 時,為 /login, 那麼就跳轉到 login.jsp 頁面,

語法為 register.jsp 時,為 /register, 那麼就跳轉到 register 頁面。

這個時候,配置 為:

	<action name="*">
			<result>/WEB-INF/content/{1}.jsp</result>
	</action>

將頁面放置在 content 文件夾下面,避免用戶直接訪問 jsp頁面。

注意,要將此 action 放置在最後, 當所有上面的action都不匹配時,才匹配這一個action.

三 Struts2的執行流程

當用戶在客戶端發送一個請求后,如常用的標準的http://localhost:8080/Struts_Hello/user/User_add.action時,

會經過前端控制器(StrutsPrepareAndExecuteFilter) 過濾器,執行一連串的過濾器鏈,然後根據user 找到了對應的package的namespape,進入到具體的package包下。 利用通配符的方式進行訪問,User_add會進行匹配相應的action,根據class和method找到是哪一個類的哪一個方法,在實例化類Action之前,會先執行攔截器。通過反射實例化類,運行方法, 方法運行成功之後,有一個返回值,這個返回值會與剛才action下的 中的name進行相應的匹配,匹配到哪一個,就執行哪一個result。 如果是diapatcher或者redirect,就显示到相應的.jsp頁面(帶有數據), 如果是chain或者redirectAction,那麼就去執行那一個方法,之後進行返回具體的視圖。

執行過程圖如下:

謝謝您的觀看!!!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

微前端與項目實施方案研究

一、前言

微前端(micro-frontends)是近幾年在前端領域出現的一個新概念,主要內容是將前端應用分解成一些更小、更簡單的能夠獨立開發、測試、部署的小塊,而在用戶看來仍然是內聚的單個產品。微前端的理念源於微服務,是將龐大的整體拆成可控的小塊,並明確它們之間的依賴關係,而它的價值在於能將低耦合的代碼與組件進行組合,基座+基礎協議模式能接入大量應用,進行統一的管理和輸出,許多公司與團隊也都在不斷嘗試和優化相關解決技術與設計方案,為這一概念的落地和推廣添磚加瓦。結合自身遇到的問題,適時引用微前端架構能起到明顯的提效賦能作用。

二、背景

目前我司擁有大量的內部系統,這些系統採用相同的技術棧,在實際開發和使用過程中,逐漸暴露出如下幾個問題:

1.有大量可復用的部分,雖然有組件庫,但是依賴版本難統一;
2.靜態資源體積過大,影響頁面加載和渲染速度;
3.應用切換目前是通過鏈接跳轉的方式實現,會有白屏和等待時長的問題,對用戶體驗不夠友好;
針對上述幾個問題,決定採用微前端架構對內部系統進行統一的管理,本文也是圍繞微前端落地的技術預研方案。

三、方案調研

目前業界有多種解決方案,有各自的優缺點,具體如下:

  • 路由轉發:路由轉發嚴格意義上不屬於微前端,多個子模塊之間共享一個導航即可 簡單,易實現 體驗不好,切換應用整個頁面刷新;

  • 嵌套 iframe:每個子應用一個 iframe 嵌套 應用之間自帶沙箱隔離 重複加載腳本和樣式;

  • 構建時組合:獨立倉儲,獨立開發,構建時整體打包,合併應用 方便依賴管理,抽取公共模塊 無法獨立部署,技術棧,依賴版本必須統一;

  • 運行時組合:每個子應用獨立構建,運行時由主應用負責應用管理,加載,啟動,卸載,通信機制 良好的體驗,真正的獨立開發,獨立部署 複雜,需要設計加載,通信機制,無法做到徹底隔離,需要解決依賴衝突,樣式衝突問題;

    開源微前端框架也有多種,例如阿里出品的qiankun,icestark,還有針對angular提出的mooa等,都能快速接入項目,但結合公司內部系統的特點,直接採用會有有些限制,例如要實現定製界面,無刷新加載應用,且不能對現有項目的開發和部署造成影響,因此決定自研相關技術。

四、架構設計

4.1 應用層

應用層包括所有接入微服務工作台的內部系統,他們各自開發與部署,接入前後沒有多大影響,只是需要針對微服務層單獨輸出打包一份靜態資源;

4.2 微服務層

微服務層作為核心模塊,擁有資源加載、路由管理、狀態管理和用戶認證管理幾大功能,具體內容將在後面詳細闡述,架構整體工作流程如下:

4.3 基礎支撐層

基礎支撐層作為基座,提供微服務運行的環境和容器,同時接入其他後端服務,豐富實用場景和業務功能;

五、技術重難點

要實現自定義微前端架構,難點在於需要管理和整合多個應用,確保應用之間獨立運行,彼此不受影響,需要解決如下幾個問題:

5.1 資源管理

5.1.1資源加載

每個應用有一個應用資源管理和註冊的文件(app.regiser.js),其中包含路由信息,應用配置信息(configs.js)和靜態資源清單,當首次切換到某應用時,首先加載app.register.js文件,完成路由和應用信息的註冊,然後根據當前瀏覽器路由地址加載對應的靜態文件,完成頁面渲染,從而將各應用的靜態資源串聯起來,其中註冊入口文件通過webpack插件來實現,具體實現如下:
FuluAppRegisterPlugin.prototype.apply = function(compiler) {
   appId = extraAppId();
   var entry = compiler.options.entry;
   if (isArray(entry)) {
            for (var i = 0; i &lt; entry.length; i++) {
                if (isIndexFile(entry[i])) { // 入口文件
                    indexFileEdit(entry[i]);
                    entry[i] = entry[i].replace(indexEntryRegx, indeEntryTemp); // 替換入口文件
                    i = entry.length;
                }
            }
    } else {
            if (isIndexFile(entry)) { // 入口文件
                indexFileEdit(entry); // 重新生成和編輯入口文件
                compiler.options.entry = compiler.options.entry.replace(indexEntryRegx, indeEntryTemp); // 替換入口文件
            }
    }
    compiler.hooks.done.tap('fulu-app-register-done', function(compilation) {
            fs.unlinkSync(tempFilePath); // 刪除臨時文件
            return compilation;
    });
    compiler.hooks.emit.tap('fulu-app-register', function(compilation) {
        var contentStr = 'window.register("'+ appId + '", {\nrouter: [ \n ' + extraRouters() + ' \n],\nentry: {\n'; // 全局註冊方法
        var entryCssArr = [];
        var entryJsArr = [];
        for (var filename in compilation.assets) {
            if (filename.match(mainCssRegx)) { // 提取css文件
                entryCssArr.push('\"' + filename + '\"');
            } else if (filename.match(mainJsRegx) || filename.match(manifestJsRegx) || filename.match(vendorsJsRegx)) { // 提取js文件
                entryJsArr.push('\"' + filename + '\"');
            }
        }
        contentStr += ('css: ['+ entryCssArr.join(', ') +'],\n'); // css資源清單
        contentStr += ('js: ['+ entryJsArr.join(', ') +'],\n }\n});\n'); // js資源清單
        compilation.assets['resources/js/' + appId + '-app-register.js'] = { // 生成appid-app-register.js入口文件
            source: function() {
                return contentStr;
            },
            size: function() {
                return contentStr.length;
            }
        };
        return compilation;
    });
};
5.1.2資源文件名
微服務輸出打包模式下,靜態資源統一打包形式以項目id開頭,形如10000092-main.js, 文件名稱的修改通過webpack的插件實現;

核心實現代碼如下:

FuluAppRegisterPlugin.prototype.apply = function(compiler) {
    ......
    compiler.options.output.filename = addIdToFileName(compiler.options.output.filename, appId);
    compiler.options.output.chunkFilename = addIdToFileName(compiler.options.output.chunkFilename, appId);
    compiler.options.plugins.forEach((c) =&gt; {
        if (c.options) {
            if (c.options.filename) {
                c.options.filename = addIdToFileName(c.options.filename, appId);
            }
            if (c.options.chunkFilename) {
                c.options.chunkFilename = addIdToFileName(c.options.chunkFilename, appId);
            }
        }
    });
   ......
};

5.2 路由管理

路由分為應用級和菜單級兩大類,應用類以應用id為前綴,將各應用區分開,避免路由地址重名的情況,菜單級的路由由各應用的路由系統自行管理,結構如下:

5.3 狀態分隔

前端項目通過狀態管理庫來進行數據的管理,為了保證各應用彼此間獨立,因此需要修改狀態庫的映射關係,這一部分需要藉助於webpack插件來進行統一的代碼層面調整,包括model和view兩部分代碼,model定義了狀態對象,view藉助工具完成狀態對象的映射,調整規則為【應用id+舊狀態對象名稱】,下面來講解一下插件的實現;

插件的實現原理是藉助AST的搜索語法匹配源代碼中的狀態編寫和綁定的相關代碼,然後加上應用編號前綴,變成符合預期的AST,最後輸出成目標代碼:
module.exports = function(source) {
      var options = loaderUtils.getOptions(this);
	stuff = 'app' + options.appId;
	isView = !!~source.indexOf('React.createElement'); // 是否是視圖層
	allFunc = [];
	var connectFn = "function connect(state) {return Object.keys(state).reduce(function (obj, k) { var nk = k.startsWith('"+stuff+"') ? k.replace('"+stuff+"', '') : k; obj[nk] = state[k]; return obj;}, {});}";
	connctFnAst = parser.parse(connectFn);
	const ast = parser.parse(source, { sourceType: "module", plugins: ['dynamicImport'] });
	traverse(ast, {
		CallExpression: function(path) {
			if (path.node.callee && path.node.callee.name === 'connect') { // export default connext(...)
				if (isArray(path.node.arguments)) {
					var argNode = path.node.arguments[0];
					if (argNode.type === 'FunctionExpression') { // connect(() => {...})
						traverseMatchFunc(argNode);
					} else if (argNode.type === 'Identifier' && argNode.name !== 'mapStateToProps') { // connect(zk)
						var temp_node = allFunc.find((fnNode) => {
							return fnNode.id.name === argNode.name;
						});
						if (temp_node) {
							traverseMatchFunc(temp_node);
						}
					}
				}
			} else if (path.node.callee && path.node.callee.type === 'SequenceExpression') {
				if (isArray(path.node.callee.expressions)) {
					for (var i = 0; i < path.node.callee.expressions.length; i++) {
						if (path.node.callee.expressions[i].type === 'MemberExpression'
							&& path.node.callee.expressions[i].object.name === '_dva'
							&& path.node.callee.expressions[i].property.name === 'connect') {
								traverseMatchFunc(path.node.arguments[0]);
								i = path.node.callee.expressions.length;
						}
					}
				}
			}
		},
		FunctionDeclaration: function(path) {
			if (path.node.id.name === 'mapStateToProps' && path.node.body.type === 'BlockStatement') {
				traverseMatchFunc(path.node);
			}
			allFunc.push(path.node);
		},
		ObjectExpression: function(path) {
			if (isView) {
				return;
			}
			if (isArray(path.node.properties)) {
				var temp = path.node.properties;
				for (var i = 0; i < temp.length; i++) {
					if (temp[i].type === 'ObjectProperty' && temp[i].key.name === 'namespace') {
						temp[i].value.value = stuff + temp[i].value.value;
						i = temp.length;
					}
				}
			}
		}
	});
	return core.transformFromAstSync(ast).code;
};

5.4 框架容器渲染

完成以上步驟的改造,就可以實現容器中的頁面渲染,這一部分涉及到組件庫框架層面的調整,大流程如下圖:

六、構建流程

6.1 使用插件

構建過程中涉及到兩款自開發的插件,分別是fulu-app-register-plugin和fulu-app-loader;

6.1.1 安裝
npm i fulu-app-register-plugin fulu-app-loader -D;
6.1.2 配置

webpack配置修改:

const FuluAppRegisterPlugin = require('fulu-app-register-plugin');
module: {
   rules: [{
         test: /\.jsx?$/,
         loader: 'fulu-app-loader',
      }
   ]
}
plugins: [
    new FuluAppRegisterPlugin(),
    ......
]

6.2.編譯

編譯過程與目前項目保持一致,相比以前,多輸出了一份微前端項目編譯代碼,流程如下:

七、遺留問題

7.1 js環境隔離

由於各應用都加載到同一個運行環境,因此如果修改了公共的部分,則會對其他系統產生不可預知的影響,目前沒有比較好的辦法來解決,後續將持續關注這方面的內容,逐漸優化達到風險可制的效果。

7.2.獲取token

目前應用切換使用重定向來完成token獲取,要實現如上所述的微前端效果,需要放棄這種方式,改用接口調用異步獲取,或者其他解決方案。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

1.5T超大空間只要6.98萬起的自主SUV車主滿意嗎

9萬首先我是五菱的忠實粉絲,第一輛車就是五菱,跑了20萬公里發動機和變速箱還沒動過,這質量是我選擇560的原因,其次我要經常拉點貨,後排放倒后空間完全可以滿足我,而且一家人出去玩也方便很多,1。8L+5AMT開起來比手動擋省力得多,雖然有時候會有些頓挫感,但畢竟不是全自動,也能接受了。

前言

有什麼車上市不到一年,就可以做到超過330000輛的銷量成績呢,沒錯它就是自主SUV陣型中的又一神車-寶駿560,對於任何品牌和車型來說,這都是一個了不起的成績,從全系沒有自動擋,到增加了5AMT,隨後又推出了1.5T的車型,為了更適應消費者需求一直在改進,那麼車主對它的評價如何呢,一起來探討一下。

上汽通用五菱-寶駿560

指導價:6.98-9.48萬

車主:菜鳥凌凌漆

購買車型:2016款 1.8L 手動精英型

裸車價格:7.68萬

五菱的車子實用性是經過市場的長時間考驗的,性價比也是較高的,圓潤飽滿的外觀是非常耐看的,雖說沒什麼太大的亮點,內飾中控布局也趨向轎車化的設計水平,無論是前後排,空間都非常寬敞,後排想怎麼坐就怎麼坐,1.8L的動力還是挺不錯的,起油比較快,操控性精準,尤其是在鄉村道路,底盤高的優勢就很明顯。

10月份提的車,目前行駛了2000多公里,油耗7.5L左右,續航能力很好。

車主:三石哥

購買車型:2016款 1.8L AMT智能手動豪華型

裸車價格:8.9萬

首先我是五菱的忠實粉絲,第一輛車就是五菱,跑了20萬公里發動機和變速箱還沒動過,這質量是我選擇560的原因,其次我要經常拉點貨,後排放倒后空間完全可以滿足我,而且一家人出去玩也方便很多,1.8L+5AMT開起來比手動擋省力得多,雖然有時候會有些頓挫感,但畢竟不是全自動,也能接受了。

11月份提的車,目前走了1800公里,市區油耗7.8左右,高速6.7左右,比之前麵包車要省油。

車主:青春的奧秘

購買車型:2016款 1.5T 手動豪華型

裸車價格:8.88萬

首先外觀時尚大氣,寶駿560 1.5T手動豪華版配置很齊全,功能強大,無鑰匙進入/啟動、倒車影像、定速巡航什麼的都有配備,動力非常充足,渦輪介入得比較早,加速很順暢,後排空間秒殺同級,北京癱什麼的隨便坐,電動助力的方向盤很輕盈,一個手也可以輕鬆操控,轉向很精準,這麼低的價格買到這麼好的車子,超值了。

9月份提的車,目前行駛4000公里,綜合油耗6.7,還是非常省油的。

總結:寶駿560一上市的歡迎程度就非常誇張,從單一的動力總成擴展到兩個,從純手動擋車型發展到增加了5AMT,現在唯一欠缺的就是全自動擋車型,上升的空間還有很大,逐步完善後再創銷量奇迹也是指日可待。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

最新研究:大氣二氧化碳總量逼近1500萬年來高峰

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

業界也認了 不是所有生質能都是碳中性 歐盟面臨減碳新挑戰

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

分類
發燒車訊

在k8s上部署日誌系統elfk

日誌系統elfk

前言

經過上周的技術預研,在本周一通過開會研究,根據公司的現有業務流量和技術棧,決定選擇的日誌系統方案為:elasticsearch(es)+logstash(lo)+filebeat(fi)+kibana(ki)組合。es選擇使用aliyun提供的es,lo&fi選擇自己部署,ki是阿里雲送的。因為申請ecs需要一定的時間,暫時選擇部署在測試&生產環境(吐槽一下,我司測試和生產公用一套k8s並且託管與aliyun……)。用時一天(前期有部署的差不多過)完成在kubernetes上部署完成elfk(先部署起來再說,優化什麼的後期根據需要再搞)。

組件簡介

es 是一個實時的、分佈式的可擴展的搜索引擎,允許進行全文、結構化搜索,它通常用於索引和搜索大量日誌數據,也可用於搜索許多不同類型的文。

lo 主要的有點就是它的靈活性,主要因為它有很多插件,詳細的文檔以及直白的配置格式讓它可以在多種場景下應用。我們基本上可以在網上找到很多資源,幾乎可以處理任何問題。

作為 Beats 家族的一員,fi 是一個輕量級的日誌傳輸工具,它的存在正彌補了 lo 的缺點fi作為一個輕量級的日誌傳輸工具可以將日誌推送到中心lo。

ki是一個分析和可視化平台,它可以瀏覽、可視化存儲在es集群上排名靠前的日誌數據,並構建儀錶盤。ki結合es操作簡單集成了絕大多數es的API,是專業的日誌展示應用。

數據採集流程圖

日誌流向:logs_data—> fi —> lo —> es—> ki。

logs_data通過fi收集日誌,輸出到lo,通過lo做一些過濾和修改之後傳送到es數據庫,ki讀取es數據庫做分析。

部署

根據我司的實際集群狀況,此文檔部署將完全還原日誌系統的部署情況。

在本地MAC安裝kubectl連接aliyun託管k8s

在客戶端(隨便本地一台虛機上)安裝和託管的k8s一樣版本的kubectl

curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.14.8/bin/linux/amd64/kubectl   
chmod +x ./kubectl 
mv ./kubectl /usr/local/bin/kubectl  
將阿里雲託管的k8s的kubeconfig 複製到$HOME/.kube/config 目錄下,注意用戶權限的問題
部署ELFK

申請一個名稱空間(一般一個項目一個名稱空間)。

# cat kube-logging.yaml 
apiVersion: v1
kind: Namespace
metadata:
  name: loging

部署es。網上找個差不多的資源清單,根據自己的需求進行適當的修改,運行,出錯就根據日誌進行再修改。

# cat elasticsearch.yaml 
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-class
  namespace: loging
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
# Supported policies: Delete, Retain
reclaimPolicy: Delete
---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: datadir1
  namespace: logging
  labels:
    type: local
spec:
  storageClassName: local-class
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/data/data1"
--- 
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch
  namespace: loging
spec:
  serviceName: elasticsearch
  selector:
    matchLabels:
      app: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      containers:
      - name: elasticsearch
        image: elasticsearch:7.3.1
        resources:
            limits:
              cpu: 1000m
            requests:
              cpu: 100m
        ports:
        - containerPort: 9200
          name: rest
          protocol: TCP
        - containerPort: 9300
          name: inter-node
          protocol: TCP
        volumeMounts:
        - name: data
          mountPath: /usr/share/elasticsearch/data
        env:
          - name: "discovery.type"
            value: "single-node"
          - name: cluster.name
            value: k8s-logs
          - name: node.name
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: ES_JAVA_OPTS
            value: "-Xms512m -Xmx512m"
      initContainers:
      - name: fix-permissions
        image: busybox
        command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
        securityContext:
          privileged: true
        volumeMounts:
        - name: data
          mountPath: /usr/share/elasticsearch/data
      - name: increase-vm-max-map
        image: busybox
        command: ["sysctl", "-w", "vm.max_map_count=262144"]
        securityContext:
          privileged: true
      - name: increase-fd-ulimit
        image: busybox
        command: ["sh", "-c", "ulimit -n 65536"]
        securityContext:
          privileged: true
  volumeClaimTemplates:
  - metadata:
      name: data
      labels:
        app: elasticsearch
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "local-class"
      resources:
        requests:
          storage: 5Gi
---
kind: Service
apiVersion: v1
metadata:
  name: elasticsearch
  namespace: loging
  labels:
    app: elasticsearch
spec:
  selector:
    app: elasticsearch
  clusterIP: None
  ports:
    - port: 9200
      name: rest
    - port: 9300
      name: inter-node

部署ki。因為根據數據採集流程圖,ki是和es結合的,配置相對簡單。

# cat kibana.yaml 
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: kibana
  namespace: loging
  labels:
    k8s-app: kibana
spec:
  replicas: 1
  selector:
    matchLabels:
      k8s-app: kibana
  template:
    metadata:
      labels:
        k8s-app: kibana
    spec:
      containers:
      - name: kibana
        image: kibana:7.3.1
        resources:
          limits:
            cpu: 1
            memory: 500Mi
          requests:
            cpu: 0.5
            memory: 200Mi
        env:
          - name: ELASTICSEARCH_HOSTS
#注意value是es的services,因為es是有狀態,用的無頭服務,所以連接的就不僅僅是pod的名字了
            value: http://elasticsearch:9200   
        ports:
        - containerPort: 5601
          name: ui
          protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: kibana
  namespace: loging
spec:
  ports:
  - port: 5601
    protocol: TCP
    targetPort: ui
  selector:
    k8s-app: kibana

配置ingress-controller。因為我司用的是阿里雲託管的k8s自帶的nginx-ingress,並且配置了強制轉換https。所以kibana-ingress也要配成https。

# openssl genrsa -out tls.key 2048
# openssl req -new -x509 -key tls.key -out tls.crt -subj /C=CN/ST=Beijing/L=Beijing/O=DevOps/CN=kibana.test.realibox.com
# kubectl create secret tls kibana-ingress-secret --cert=tls.crt --key=tls.key

kibana-ingress配置如下。提供兩種,一種是https,一種是http。

https:
# cat kibana-ingress.yaml 
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kibana
  namespace: loging
spec:
  tls:
  - hosts:
    - kibana.test.realibox.com
    secretName: kibana-ingress-secret
  rules:
  - host: kibana.test.realibox.com
    http:
      paths:
      - path: /
        backend:
          serviceName: kibana
          servicePort: 5601

http:
# cat kibana-ingress.yaml 
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kibana
  namespace: loging
spec:
  rules:
  - host: kibana.test.realibox.com
    http:
      paths:
      - path: /
        backend:
          serviceName: kibana
          servicePort: 5601

部署lo。因為lo的作用是對fi收集到的日誌進行過濾,需要根據不同的日誌做不同的處理,所以可能要經常性的進行改動,要進行解耦。所以選擇以configmap的形式進行掛載。

# cat logstash.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: logstash
  namespace: loging
spec:
  replicas: 1
  selector:
    matchLabels:
      app: logstash
  template:
    metadata:
      labels:
        app: logstash
    spec:
      containers:
      - name: logstash
        image: elastic/logstash:7.3.1
        volumeMounts:
        - name: config
          mountPath: /opt/logstash/config/containers.conf
          subPath: containers.conf
        command:
        - "/bin/sh"
        - "-c"
        - "/opt/logstash/bin/logstash -f /opt/logstash/config/containers.conf"
      volumes:
      - name: config
        configMap:
          name: logstash-k8s-config
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: logstash
  name: logstash
  namespace: loging
spec:
  ports:
    - port: 8080       
      targetPort: 8080
  selector:
    app: logstash
  type: ClusterIP

# cat logstash-config.yaml 
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: logstash
  name: logstash
  namespace: loging
spec:
  ports:
    - port: 8080       
      targetPort: 8080
  selector:
    app: logstash
  type: ClusterIP
---

apiVersion: v1
kind: ConfigMap
metadata:
  name: logstash-k8s-config
  namespace: loging
data:
  containers.conf: |
    input {
      beats {
        port => 8080  #filebeat連接端口
      }
    }
    output {
      elasticsearch {
        hosts => ["elasticsearch:9200"]  #es的service
        index => "logstash-%{+YYYY.MM.dd}"
      }
    }
注意:修改configmap 相當於修改鏡像。必須重新apply 應用資源清單才能生效。根據數據採集流程圖,lo的數據由fi流入,流向es。

部署fi。fi的主要作用是進行日誌的採集,然後將數據交給lo。

# cat filebeat.yaml 
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-config
  namespace: loging
  labels:
    app: filebeat
data:
  filebeat.yml: |-
    filebeat.config:
      inputs:
        # Mounted `filebeat-inputs` configmap:
        path: ${path.config}/inputs.d/*.yml
        # Reload inputs configs as they change:
        reload.enabled: false
      modules:
        path: ${path.config}/modules.d/*.yml
        # Reload module configs as they change:
        reload.enabled: false
    # To enable hints based autodiscover, remove `filebeat.config.inputs` configuration and uncomment this:
    #filebeat.autodiscover:
    #  providers:
    #    - type: kubernetes
    #      hints.enabled: true
    output.logstash:
      hosts: ['${LOGSTASH_HOST:logstash}:${LOGSTASH_PORT:8080}']   #流向lo
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-inputs
  namespace: loging
  labels:
    app: filebeat
data:
  kubernetes.yml: |-
    - type: docker
      containers.ids:
      - "*"
      processors:
        - add_kubernetes_metadata:
            in_cluster: true
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: filebeat
  namespace: loging
  labels:
    app: filebeat
spec:
  selector:
    matchLabels:
      app: filebeat
  template:
    metadata:
      labels:
        app: filebeat
    spec:
      serviceAccountName: filebeat
      terminationGracePeriodSeconds: 30
      containers:
      - name: filebeat
        image: elastic/filebeat:7.3.1
        args: [
          "-c", "/etc/filebeat.yml",
          "-e",
        ]
        env:   #注入變量
        - name: LOGSTASH_HOST
          value: logstash
        - name: LOGSTASH_PORT
          value: "8080"
        securityContext:
          runAsUser: 0
          # If using Red Hat OpenShift uncomment this:
          #privileged: true
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 100Mi
        volumeMounts:
        - name: config
          mountPath: /etc/filebeat.yml
          readOnly: true
          subPath: filebeat.yml
        - name: inputs
          mountPath: /usr/share/filebeat/inputs.d
          readOnly: true
        - name: data
          mountPath: /usr/share/filebeat/data
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      volumes:
      - name: config
        configMap:
          defaultMode: 0600
          name: filebeat-config
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: inputs
        configMap:
          defaultMode: 0600
          name: filebeat-inputs
      # data folder stores a registry of read status for all files, so we don't send everything again on a Filebeat pod restart
      - name: data
        hostPath:
          path: /var/lib/filebeat-data
          type: DirectoryOrCreate
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: filebeat
subjects:
- kind: ServiceAccount
  name: filebeat
  namespace: loging
roleRef:
  kind: ClusterRole
  name: filebeat
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: filebeat
  labels:
    app: filebeat
rules:
- apiGroups: [""] # "" indicates the core API group
  resources:
  - namespaces
  - pods
  verbs:
  - get
  - watch
  - list
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: filebeat
  namespace: loging
  labels:
    app: filebeat
---

至此完成在k8s上部署es+lo+fi+ki ,進行簡單驗證。

驗證

查看svc、pod、ingress信息

# kubectl get svc,pods,ingress -n loging
NAME                    TYPE        CLUSTER-IP        EXTERNAL-IP   PORT(S)             AGE
service/elasticsearch   ClusterIP   None              <none>        9200/TCP,9300/TCP   151m
service/kibana          ClusterIP   xxx.168.239.2xx   <none>        5601/TCP            20h
service/logstash        ClusterIP   xxx.168.38.1xx   <none>        8080/TCP            122m

NAME                            READY   STATUS    RESTARTS   AGE
pod/elasticsearch-0             1/1     Running   0          151m
pod/filebeat-24zl7              1/1     Running   0          118m
pod/filebeat-4w7b6              1/1     Running   0          118m
pod/filebeat-m5kv4              1/1     Running   0          118m
pod/filebeat-t6x4t              1/1     Running   0          118m
pod/kibana-689f4bd647-7jrqd     1/1     Running   0          20h
pod/logstash-76bc9b5f95-qtngp   1/1     Running   0          122m

NAME                        HOSTS                       ADDRESS        PORTS     AGE
ingress.extensions/kibana   kibana.test.realibox.com   xxx.xx.xx.xxx   80, 443   19h
web配置

配置索引

發現

至此算是簡單完成。後續需要不斷優化,不過那是後事了。

問題總結

這應該算是第一次親自在測試&生產環境部署應用了,而且是自己很不熟悉的日子系統,遇到了很多問題,需要總結。

  1. 如何調研一項技術棧;
  2. 如何選定方案;
  3. 因為網上幾乎沒有找到類似的方案(也不曉得別的公司是怎麼搞的,反正網上找不到有效的可能借鑒的)。需要自己根據不同的文檔總結嘗試;
  4. 一個組件的標籤盡可能一致;
  5. 如何查看公司是否做了端口限制和https強制轉換;
  6. 遇到IT的事一定要看日誌,這點很重要,日誌可以解決絕大多數問題;
  7. 一個人再怎麼整也會忽略一些點,自己先嘗試然後請教朋友,共同進步。
  8. 項目先上線再說別的,目前是這樣,一件事又百分之20的把握就可以去做了。百分之80再去做就沒啥意思了。
  9. 自學重點學的是理論,公司才能學到操作。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

分類
發燒車訊

助長毀林?巴西銷往歐洲的黃豆 至少20%來自森林砍伐

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

分類
發燒車訊

Redis學習筆記(十八) 集群(下)

複製和故障轉移

Redis集群中的節點分為主節點(master)和從節點(slave),其中主節點用於處理槽,而從節點則用於複製某個主節點,並在被複制 的主節點下線時,代替下線主節點繼續處理命令請求。

設置從節點:CLUSTER REPLICATE < node_id >可以讓接收命令的節點稱為node_id 所指定節點的從節點,並開始對主節點進行複製。

1)接收到該命令的節點首先會在自己的clusterState.nodes字典中找到node_id所對應節點的clusterNode結構,並將自己的clusterState.myself.slaveof指針指向這個結構,以此來記錄這個節點正在複製的主節點:

struct clusterNode{
    //如果這個時一個從節點,那麼指向主節點
    struct clusterNode *slaveof;
}

2)節點修改自己的clusterState.myself.flags中的屬性,關閉原本的REDIS_NODE_MASTER標識,打開REDIS_NODE_SLAVE標識,標識這個節點已經由原來的主節點變成了從節點。

3)節點會調用複製代碼,根據clusterState.myself.slaveof指向clusterNode結構所保存的IP地址和端口號,對節點進行複製。

一個節點稱為從節點,並開始複製某個主節點這一信息會通過消息發送給集群中的其他節點,最終集群中的所有節點都會知道某個從節點正在複製某個主節點。

集群中的所有節點都會在代表主節點的clusterNode結構的slaves屬性和numslaves屬性中記錄正在複製這個主節點的從節點名單:

struct clusterNode{
    //正在複製這個主節點的從節點數量
    int numslaves;
    //數組,每個數組項指向一個正在複製這個主節點的從節點的clusterNode
    struct clusterNode **slaves;
}

集群中的每個節點都會定期地向集群中的其他節點發送PING消息,一次來檢測對方是否在線,如果接收PING消息的節點沒有在規定的時間內,向發送PING消息的節點返回PONG消息,那麼發送PING消息的節點就會將階段后PING消息的節點標記為疑似下線(PFAIL)。

集群中的各個節點會通過相互發送消息的方式來交換集群中各個節點的狀態信息:某個節點處於在線狀態、疑似下線、已下線狀態。

當一個主節點A通過罅隙得知主節點B認為主節點C進入疑似下線狀態時,主節點A會在自己的clusterState.nodes字典中找到主節點C所對應的clusterNode結構,並將主節點B的下線報告添加到clusterNode結構的fail_reports鏈表中

status clusterNode{
    list *fali_reports;//鏈表,記錄所有其他節點對該節點的下線報告
};

下線報告結構:

 

struct c;isterNodeFailReport{
    //報告目標節點已經下線的節點
    struct clusterNode *node;
    //最後一個從node節點收到下線報告的時間(程序使用這個時間戳來檢查下線報告是否過期)
    mstime_t time;
} typedef clusterNodeFailReport;

如果集群里半數以上負責處理槽的主節點都將某個主節點x報告未疑似下線,那麼這個主節點x將被標記未已下線,將主節點x標記為已下線的節點會向集群廣播一條關於主節點x的FAIL罅隙,所有收到這條罅隙的節點都會立即將主節點x標記為已下線。

故障轉移的步驟:

1)複製下線主節點的所有從節點裏面,會有一個從節點被選中,

2)被選中的從節點會執行SLAVEOF no one命令,成為新的主節點。

3)新的主節點會撤銷所有對已下線主節點的槽指派,並將這些槽指派給自己。

4)新的主節點向集群廣播一條PONG消息,這條消息讓其他集群中的其他節點立即知道這個節點已經由從節點變為主節點,並且這個主節點已經接管了原本已下線節點負責處理的槽。

5)新的主節點開始接收和自己負責處理的槽有關的命令請求,故障轉移完成。

選舉新的主節點:

1)集群的配置紀元是一個計數器。他的初始值為0;

2)當集群中的某個節點開始一次故障轉移操作時,集群配置紀元的值會被加1。

3)集群裏面每個負責處理槽的主節點都有一次投票機會,而第一個向主節點要求投票的從節點將獲得主節點的投票。

4)當從節點發現自己正在複製的主節點進入下線狀態時,從節點會向集群官博一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到這個消息、並且具有投票權的主節點向這個從節點投票。

5)如果一個主節點具有投票權,並且這個主節點尚未投票給其他從節點,那麼主節點將向要求投票的從節點返回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示這個主節點支持從節點成為新的主節點。

6)每個參与選舉的從節點都會收到CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,並根據自己收到了多少條這種消息來統計自己獲得了多少主節點支持。

7)如果集群中有N個具有投票權的主節點,那麼當一個從節點大於等於N/2+1張支持票時,這個從節點就當選成為新的主節點。

8)如果在一個配置紀元裏面沒有從節點收集到足夠多的支持票,那麼集群進入下一個紀元,再次進行選舉,直到選出新的主節點為止。

 

消息

集群中各個節點通過發送和接收消息來進行通信,我們稱發送消息的節點為發送者,接收消息的節點為接收者:

1)MEET消息,當發送者接到客戶端發送的CLUSTER MEET命令時,發送者會向接收者發送MEET消息,請求接收者加入到發送者當前所處的集群裏面。

2)PING消息,集群裏面的每個節點默認每隔一秒鐘就會從已知節點列表中隨機選出五個節點,然後對這五個節點中最長時間沒有發送過PING消息的節點發送PING消息,以此檢測被選中的節點是否在線。除此之外,如果節點A最後一次收到節點B發送的PONG消息的時間,距離當前時間已超過了節點A的cluster-node-timeout選項設置時長的一半,那麼節點A也會向節點B發送PING消息,這可以防止節點A因長時間沒有隨機選中節點B作為PING消息的發送對象而導致節點B的信息更新滯后。

3)PONG消息,當接收者收到發送者發來的MEET消息或者PING時,為了向發送者確認這條MEET、PING消息已到達,接收者會向發送者返回一條PONG消息。另外,一個節點也可以通過向集群發送集群廣播自己的PONG消息來讓集群中的其他節點立即刷新關於這個節點的認識。

4)FAIL消息,當一個主節點A判斷另一個主節點B已經進入FAIL狀態時,節點A會會向集群廣播一條關於節點B的FAIL消息,所有接收到這條消息的節點都會立即將節點B標記為已下線。

5)PUBLISH消息,當節點接收到一個PUBLISH命令時,節點會執行這個命令,並向集群廣播一條PUBLISH消息,所有接收到這條PUBLISH消息的節點都會執行相同的PUBLISH命令。

一條消息由消息頭(header)和消息正文(data組成)

消息頭:

typedef struct {
    //消息的長度(消息頭的長度和消息正文的長度)
    uint32_t totlen;
    //消息的類型
    uint16_t type;
    //消息正文包含的節點信息數量
    //只有發送MEET、PING、PONG這三種Gossip協議消息時使用
    uint16_t count;
    
    //薩松這所處的配置紀元
    uint64_t currentEpoch;
    //如果發送者是一個主節點,那麼這裏面記錄的時發送者的配置紀元
    //如果發送者時一個從節點,那麼這裏面記錄的時發送者正在複製的主節點的配置紀元
    uint64_t configEpoch;
    //發送者的名稱(ID)
    char sender[REDIS_CLUSTER_NAMELEN];
    //發送者目前的槽指派信息
    unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
    //如果發送者是一個從節點,記錄的是發送者正在複製的主節點的名稱
    //如果發送者是一個主節點,那麼這裏記錄的是REDIS_NODE_NULL_NAME
    char slaveof[REDIS_CLUSTER_NAMELEN];
    //發送者的端口號
    uint16_t port;
    //發送者的標識值
    uint16_t flags;
    //發送者所處集群的狀態
    unsigned char state;
    //消息正文
    union clusterMsgData data;
} clusterMsg;

clusterMsg.data 結構:

union clusterMsgData{
    //MEET PING PONG 消息正文
    struct{
        //每條MEET PING PONG消息都包含兩個 clusterMsgDataGossip 結構
        clusterMsgDataGossip gossip[1]
    } ping;
    //FAIL 消息正文
    struct{
        clusterMsgDataFail about;
    } fali;
    
    //PUBLISH消息正文
    struct{
        clusterMsgDataPublish msg;
    } publish;
}

clusterMsgDataGossip結構記錄了選中節點的名字,發送者與被選中節點最後一次發送和接收PING消息和PONG消息的時間戳,被選中節點的IP地址和端口號,以及被選中節點的標識值:

typedef struct {
    //節點的名字
    char nodename[REDIS_CLUSTER_NAMELEN];
    //最後一次向該節點發送PING消息的時間戳
    uint32_t ping_sent;
    //最後一次從該 節點接收到PONG消息的時間戳
    uint32_t pong_received;
    //節點的IP地址
    char ip[16];
    //節點的端口號
    uint16_t port;
    //節點的標識值
    uint16_t flags;
} clusterMsgDataGossip;

每天學一點,總會有收穫。

 

說明:尊重作者知識產權,文中內容參考《Redis設計與實現》,僅在此做學習與大家分享。

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

分類
發燒車訊

10萬以下產品力最強的兩款SUV!詳解風神AX5和遠景SUV!

未來肯定不能以價格取勝。那麼要考慮的就是要從質量與技術可靠為主了。而且有了AX7在前面擋着,未來AX5隻要在宣傳上做得出色,未來還是可以期待的。而遠景SUV換裝了全新的家族式造型設計,與吉利汽車其他車型有了很好的延續性。

目前SUV市場非常紅火,但對於國內的消費者來說;他們很多時候對SUV的購車預算大多數會考慮在10萬以下的車型,而這個區間最火熱的要屬最火熱的遠景SUV了。遠景SUV作為一款上市多年的車型,憑藉著良好的口碑與超高的性價比在三四線城市當中一直都受到追捧。其他品牌看它這麼紅火,肯定心有不甘;醞釀着推出競品與其競爭。在這個背景下,東風風神AX5應該是與它最接近的一款車型了。

總結:AX5作為市場的新丁,未來要遇到的挑戰很多;首先它目前的市場肯可度比較一般,而且相對性價比較高的遠景SUV來說,價格相對高一些;未來肯定不能以價格取勝。那麼要考慮的就是要從質量與技術可靠為主了;而且有了AX7在前面擋着,未來AX5隻要在宣傳上做得出色,未來還是可以期待的。

而遠景SUV換裝了全新的家族式造型設計,與吉利汽車其他車型有了很好的延續性。但它的車尾卻與前臉不太協調,但改款之後提價的策略也被不少消費者詬病;這是吉利急需要改變的不好影響。當然目前吉利汽車一系列熱門車型會把整個市場口碑營造上去,但遠景SUV卻不能以此為傲。雖不說前有前敵,但肯定後邊會有不少追兵。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?