日產傳退出EV電池生產業務、子公司AES賣中國GSR

朝日新聞22日報導,日產汽車(Nissan)將退出電動車(EV)用電池的生產業務,旗下車用電池生產子公司「Automotive Energy Supply(以下簡稱AES)」將出售給中國投資基金「GSR」,出售額預估為1,000億日圓,雙方預計會在今年夏天達成共識並對外發表。日產在退出EV電池生產業務後,會將資源集中在最先端電池的研發上。

報導指出,鋰離子電池是決定EV競爭力的關鍵,而日系廠商原先是該領域的霸主,不過近年來,南韓、中國廠商呈現顯著成長,加上AES銷售通路有限,因此日產研判擁有EV電池生產業務不具備太多好處。

AES為日產、NEC分別出資51%、49%於2007年設立的公司,主要生產日產電動車Leaf或油電混合車(HV)所需的鋰離子電池。

根據調查公司Techno Systems Research指出,2014年Panasonic車用鋰離子電池全球市佔率高達47%,而2015年雖維持首位、但市佔率萎縮至34%;反觀「其他廠商」市佔率從14%飆增至33%,而在「其他廠商」中、比亞迪(BYD)等中國廠商佔了大多數,LG化學、三星SDI等南韓廠商也在後猛追。

中國對電池市場虎視眈眈,成長最快的電池廠「寧德時代新能源科技股份」(簡稱寧德時代或CATL)已誓言要在2020年底前,使電池產量超越特斯拉(Tesla)、Panasonic合資的「Gigafactory」超大電池廠。

英國金融時報3月5日報導,北京已呼籲企業在2020年底前將車用電池產能拉高一倍,還鼓勵業者前往海外投資。高盛預估,鋰電池將是未來10年的關鍵技術,估計到了2025年市場總值上看400億美元、且主要會由中國主導。

寧德時代行銷主任Neil Yang預測,雖然超越日本、南韓業者並非易事,但預估未來10年內,全球只會剩下10家鋰電池製造商,其中60%的市場會由前三大業者拿下。他也透露寧德時代要將觸角探至全球的野心,聲稱該公司希望跟特斯拉合作,也一直在跟通用汽車(GM)接洽,而福斯(Volkswagen)、BMW則已都是他們的客戶。

寧德時代預計要在2020年將鋰電池總產能拉升至50GWh,而中國整體的鋰電池產能則合計會達121GWh。相較之下,特斯拉則希望在明(2018)年將鋰電池產能拉升至35GWh。每1個GWh的電力,可供應40,000輛電動車、每部行駛100公里所需。中國已在2013年超越南韓成為全世界最大的鋰電池供應國。

(本文內容由MoneyDJ授權使用。圖片出處:public domain CC0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Gogoro 太陽能電池交換站,在新北八里驅動示範

新創科技公司 Gogoro 睿能創意股份有限公司,28 日宣布其與新北市政府合作的首座太陽能電池交換站,正式在新北市八里區開始營運。隨著這個搭配太陽能面板的電池交換站的啟動,Gogoro 對永續能源的承諾邁出更關鍵的一步。   這座位於八里十三行博物館附近(新北市八里區文昌路與文昌一街交叉口)的 Gogoro 八里公兒四電池交換站配置 2.3kW 太陽能面板,依據天氣環境等因素,每天可以產生大約 6.21 kWH 的電力,是第一座以再生能源提供電力的 GoStation 電池交換站。太陽能電力不但可以提供電池交換站的 Gogoro 電池使用,降低發電時所產生的二氧化碳排放,更可以與全台電力網路連結,參與整體網路的電量調節。   Gogoro 執行長陸學森表示 :「Gogoro 致力發展潔淨的智慧能源,希望透過具備能源調度能力的智慧電網,成為城市的電力調節樞紐,以促成電力平衡。與新北市政府合作的太陽能八里電池交換示範站為我們上述的目標邁出了重要的一步。」   新北市政府則表示,市府一直在積極尋找降低溫室氣體排放、改善空氣污染的積極方法,與 Gogoro 合作成立全世界第一座太陽能電池交換站具有標竿式的意義。電動機車零排放的特性不但可以提升市區的空氣品質,以再生能源為來源的電力,更可以降低交通工具所帶來的碳排放。未來也將持續努力發展以再生能源補充電力的相關設施,做為新北市重要的基礎設施。   這座太陽能電池交換示範站,設有物聯網智慧平台,透過分析供電情況的螢幕,說明了包括減少碳排量,減少樹木砍伐面積,綠能總儲電量,城市電網 ,太陽能發電量等訊息,讓每名換電的民眾,清楚的知道,自己對環境的貢獻度,也具有相當程度的教育意義。   Gogoro 目前擁有 25,000 名車主,總共累計將近 500 萬次的電池交換,9,000 萬公里的總里程數,已經替地球減少 720 萬公斤的二氧化碳排放,隨著未來再生能源比例逐漸提升,Gogoro 的車主們將更對地球與環境產生更多的正面影響力。Gogoro 為鼓勵車主響應綠能電池交換站,也設計了特別的機制。凡是造訪八里公兒四站,並完成電池交換的車主,即可在 Gogoro App 上獲得「阿波羅的力量」徽章。   (合作媒體:。圖片出處:科技新報)  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

.NET資源泄露與處理方案

.NET雖然擁有強大易用的垃圾回收機制,但並不是因為這樣,你就可以對資源管理放任不管,其實在稍不注意的時候,可能就造成了資源泄露,甚至因此導致系統崩潰,到那時再來排查問題就已經是困難重重。

==========    ==========

一、知識點簡單介紹

常見的資源泄露有:

  • 內存泄漏:非託管資源沒有釋放、非靜態對象註冊了靜態實例。
  • GDI泄露:字體。
  • 句柄泄露:Socket或線程。
  • 用戶對象泄露:移除的對象未釋放。

二、具體實例

1. 內存泄漏

很常見的現象是分不清哪些對象需要釋放,對於控件、Stream等一些非託管資源也只管新增,卻沒有釋放,功能是實現了,卻埋了顆不小的雷。

private void button1_Click(object sender, EventArgs e)
{
    for(int i=0;i<1000;i++)
        this.Controls.Add(new TabPage());
}
private void button1_Click(object sender, EventArgs e)
{
    new Form2.ShowDialog();
}

如果你覺得寫這樣的代碼很Cool,很簡潔,你在項目中也有這麼寫代碼,那你就碰到大麻煩了,你試試在上面Form2中開個大一點的數組來檢查內存,然後運行,按幾下按鈕,你就會發現,內存一直增加,即使你調用了GC也無濟於事。所以,對於此類非託管資源要記住釋放,用完即廢可以採用using關鍵字。

public Form2()
{
    InitializeComponent();
    MyApp.FormChanged += FormChanged;
}

上面這個例子中,MyApp是一個靜態類,如果在實例對象中向這種類裏面註冊了事件,而又沒有取消註冊,這樣也會遇到大麻煩,即使在外部已經記得調用了Form2的Dispose也是沒用的。

解決方案

  • 注意託管資源和非託管資源的釋放區別,非託管資源是需要手動釋放的。
  • ,如上面的ShowDialog問題。(using中還起到了try-catch的作用,避免由於異常未調用Dispose的情況)
  • 使用UnLoad事件或者析構函數,對註冊的全局事件進行取消註冊。
  • 特別注意自定義組件的穩定性更重要,發生問題時影響也更廣。

2. GDI泄露

一般會跟字體相關,例如我曾在Android上用Cocos2d做一個小遊戲時頻繁地切換字體、Dev控件的Font屬性賦值也會有這種現象。

XXX.Font = new Font(...)

解決方案

  • 這個問題我目前是採用字體池來解決,類似線程池的概念,相同Key值取同一個對象。若有更好方案歡迎留言討論

3. 句柄泄露

一般跟Socket和Thread(線程)有關

for(int i=0;i<1000;i++){
    new Thread(()=>{
        Thread.Sleep(1000);
    }).Start();
}

解決方案

  • Socket的場景暫時沒遇到。
  • 線程問題採用線程池相關的輔助類能有效解決,例如ThreadPool、Task、Parallel。

4. 用戶對象泄露

一般跟移除的對象未釋放有關

private void button1_Click(object sender, EventArgs e)
{
    tab.Remove(tabPage);
}

三、最後特別奉送一個內存釋放的大招

[DllImport("kernel32.dll", EntryPoint = "SetProcessWorkingSetSize")]
public static extern int SetProcessWorkingSetSize(IntPtr process, int minSize, int maxSize);
/// <summary>    
/// 釋放內存    
/// </summary>    
public static void ClearMemory()
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    if (Environment.OSVersion.Platform == PlatformID.Win32NT)
    {
        SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1);
    }
}

調用以上API能讓你的內存一下爆減,是不是很給力,一調用內存就降下來了。But,先別高興太早,這其實是偽釋放,只是暫時解決內存大量泄漏導致系統崩潰的應急處理方案。具體原因參考:,關鍵信息:物理內存轉虛擬內存,涉及磁盤讀寫。好處壞處都貼出來了,是否需要使用請君自己斟酌。

四、總結

實際上由於各個開發人員的水平跟接觸面不同,又沒有經過統一的培訓(各個人對資源釋放的理解與關注度不同,或者寫代碼時就沒考慮內存未被釋放這種問題),發現問題的時候項目往往已經做到了一個階段,系統也比較龐大了,這種時候才發現內存泄露的問題確實是很頭疼的。

  • 資源泄露的場景往往是相互關聯的,發生最多的就是內存泄漏,而除了寫法可能有問題外,也可能是因為句柄泄露或用戶對象泄露引起的。

五、參考資料

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

AutoCad 二次開發 文字鏡像

AutoCad 二次開發 文字鏡像

參考: 在autocad中如果使用Mirror命令把塊參照給鏡像了(最終得到一個對稱的塊),塊裏面的文字包括DBText和MText以及標註上面的文字都會被對稱,變得不易閱讀。而在單個字體實體和標註實體鏡像的時候只要設置系統變量mirrtext為0鏡像后的文字就不會與原文字對稱變成我們未學習過的文字了。   所以我們在鏡像塊的時候就可以先把塊炸開是用快捷鍵X,或者輸入explode,然後在使用鏡像命令。之後在把對稱后的實體集合組成一個新的塊。不過這樣操作十分的繁瑣,我覺得其中這樣做的優勢是mirror時的jig操作可以很方便的預先知道我們想要的對稱后的結果。但如果用代碼實現這種jig操作,我覺得有點複雜,目前我還不知道怎麼實現。   我要講的主要就是用代碼來實現塊的鏡像。難點就在與文字的鏡像,和標註的鏡像。這篇文章先講文字的鏡像。文字鏡像的主要步驟分為: 1.找到鏡像前文字邊界的四個角,這四個角構成了一個矩形,我們要求得這個矩形的長和寬所代表的向量。 2.判斷文字鏡像后的方向,如果是偏向朝Y軸鏡像,那麼文字鏡像后的方向是沿着X軸翻轉的,如果是偏向朝X軸鏡像,那麼文字鏡像后的方向是沿着X軸翻轉的。這裏我以沿着Y軸鏡像為例子。 3.移動鏡像后切被翻轉后的文字,這裏也是根據鏡像軸的不同,需按不同的向量來移動。   詳細情況見圖: 圖中左邊是要鏡像的文字,文字上的藍色線,和黃色線是我調試的時候加入的,黃線左端是 pt1,右端是pt2,藍線左端是pt3,右端是pt4。 中間的豎線是Y軸鏡像線,右邊就是不同情況下鏡像后的文字。其中黃色部分表示正確的鏡像結果,紅色部分表示:鏡像后延第一個步驟移動后求得的向量移動了文字的position但是沒翻轉的結果。黑色部分表示:鏡像后翻轉了文字但文字的position沒有按向量移動的結果。 下面我就來仔細分析一下代碼: 要實現第一步驟,前提是要有一段P/Invoke的代碼: 其中 引入的acdb22.dll是 autocad2018中的版本,不同版本,這個dll後面的数字不一樣。我們可以到cad安裝目錄下查找acdb幾個字,找到後面帶数字的就是了,64位的安裝目錄默認位置:C:\Program Files\Autodesk\AutoCAD 2018。這兩個函數一個是32位,一個是64位,具體用哪個後面的代碼會自動判斷。這個函數作用我覺得主要是求 這個name。   這裏用到了accore.dll,有的cad版本沒有這個dll,就用acad.exe代替就可以了。上面的acdbEntGet主要是根據entity的名字求的entity實體的Intptr,下面的函數時求的文字邊界對角點,這裏注意,我把這個兩個點用直線打印在cad空間里,發現它時在原點,沒旋轉的線,但其實文字不的position不在原點,也帶有旋轉角度。後面要求的文字邊界向量就是根據這兩個點來的。 上面求得的pt1,pt2 經過:
pt1 = pt1.TransformBy(rotMat).Add(dbText.Position.GetAsVector());
pt2 = pt2.TransformBy(rotMat).Add(dbText.Position.GetAsVector()); 這種操作就得到了第一幅圖中的黃線。 在經過這樣的操作,得到的pt3 和pt4就是第一幅圖的藍線。這其中的rotDir和linDir就是我們要求得的寬和長代表的向量了,然後在把它給鏡像了得到的mirRotDir和mirLinDir就是鏡像后的文字要移動的向量了,這裏第一步就結束了。 第二步,第三步:   大的話,就說明文字需要朝X軸翻轉,所以這裏的IsMirroredInX=true就代表需要朝X軸翻轉。 緊接着下面句,如果沒加mirLineDir這個向量,就會出現第一幅圖中的畫黑線的情況,如果不加IsMirrorInX就會出現畫紅線的情況。 到這裏就全部結束了。 下面給出所有代碼:

public class MyMirror
    {
        Document Doc = Application.DocumentManager.MdiActiveDocument;
        Editor Ed = Application.DocumentManager.MdiActiveDocument.Editor;
        Database Db = Application.DocumentManager.MdiActiveDocument.Database;

        List<Entity> list = new List<Entity>();
        List<ObjectId> listOId = new List<ObjectId>();

        [CommandMethod("testM")]

        public void MirrorTextCmd()

        {

            Document doc = Application.DocumentManager.MdiActiveDocument;

            Database db = doc.Database;

            Editor ed = doc.Editor;



            //Entity selection

            PromptEntityOptions peo = new PromptEntityOptions(

                "\nSelect a text entity:");



            peo.SetRejectMessage("\nMust be text entity...");

            peo.AddAllowedClass(typeof(DBText), true);



            PromptEntityResult perText = ed.GetEntity(peo);



            if (perText.Status != PromptStatus.OK)

                return;



            peo = new PromptEntityOptions("\nSelect a mirror line:");

            peo.SetRejectMessage("\nMust be a line entity...");

            peo.AddAllowedClass(typeof(Line), true);



            PromptEntityResult perLine = ed.GetEntity(peo);



            if (perLine.Status != PromptStatus.OK)

                return;



            using (Transaction tr = db.TransactionManager.StartTransaction())

            {

                Line line = tr.GetObject(perLine.ObjectId, OpenMode.ForRead)

                    as Line;



                Line3d mirrorLine = new Line3d(

                    line.StartPoint,

                    line.EndPoint);



                MirrorText(perText.ObjectId, mirrorLine);



                tr.Commit();

            }

        }



        void MirrorText(ObjectId oId, Line3d mirrorLine)

        {

            Database db = oId.Database;



            using (Transaction tr = db.TransactionManager.StartTransaction())

            {

                // Get text entity

                DBText dbText = tr.GetObject(oId, OpenMode.ForRead)

                    as DBText;



                // Clone original entity

                DBText mirroredTxt = dbText.Clone() as DBText;



                // Create a mirror matrix

                Matrix3d mirrorMatrix = Matrix3d.Mirroring(mirrorLine);



                // Do a geometric mirror on the cloned text

                mirroredTxt.TransformBy(mirrorMatrix);



                // Get text bounding box

                Point3d pt1, pt2, pt3, pt4;

                GetTextBoxCorners(

                    dbText,

                    out pt1,

                    out pt2,

                    out pt3,

                    out pt4);



                // Get the perpendicular direction to the original text

                Vector3d rotDir =

                    pt4.Subtract(pt1.GetAsVector()).GetAsVector();



                // Get the colinear direction to the original text

                Vector3d linDir =

                    pt3.Subtract(pt1.GetAsVector()).GetAsVector();



                // Compute mirrored directions

                Vector3d mirRotDir = rotDir.TransformBy(mirrorMatrix);

                Vector3d mirLinDir = linDir.TransformBy(mirrorMatrix);



                //Check if we need to mirror in Y or in X

                if (Math.Abs(mirrorLine.Direction.Y) >

                    Math.Abs(mirrorLine.Direction.X))

                {

                    // Handle the case where text is mirrored twice

                    // instead of doing "oMirroredTxt.IsMirroredInX = true"

                    mirroredTxt.IsMirroredInX = !mirroredTxt.IsMirroredInX;

                    mirroredTxt.Position = mirroredTxt.Position + mirLinDir;

                }

                else

                {

                    mirroredTxt.IsMirroredInY = !mirroredTxt.IsMirroredInY;

                    mirroredTxt.Position = mirroredTxt.Position + mirRotDir;

                }



                // Add mirrored text to database

                //btr.AppendEntity(mirroredTxt);

                //tr.AddNewlyCreatedDBObject(mirroredTxt, true);

                //list.Add(mirroredTxt);
                mirroredTxt.ToSpace();
                tr.Commit();

            }

        }
        #region p/Invoke


        public struct ads_name
        {

            public IntPtr a;

            public IntPtr b;

        };



        // Exported function names valid only for R19



        [DllImport("acdb22.dll",

            CallingConvention = CallingConvention.Cdecl,

            EntryPoint = "?acdbGetAdsName@@YA?AW4ErrorStatus@Acad@@AAY01JVAcDbObjectId@@@Z")]

        public static extern int acdbGetAdsName32(

            ref ads_name name,

            ObjectId objId);



        [DllImport("acdb22.dll",

            CallingConvention = CallingConvention.Cdecl,

            EntryPoint = "?acdbGetAdsName@@YA?AW4ErrorStatus@Acad@@AEAY01_JVAcDbObjectId@@@Z")]

        public static extern int acdbGetAdsName64(

            ref ads_name name,

            ObjectId objId);



        public static int acdbGetAdsName(ref ads_name name, ObjectId objId)

        {

            if (Marshal.SizeOf(IntPtr.Zero) > 4)

                return acdbGetAdsName64(ref name, objId);



            return acdbGetAdsName32(ref name, objId);

        }



        [DllImport("accore.dll",

            CharSet = CharSet.Unicode,

            CallingConvention = CallingConvention.Cdecl,

            EntryPoint = "acdbEntGet")]

        public static extern System.IntPtr acdbEntGet(

            ref ads_name ename);



        [DllImport("accore.dll",

            CharSet = CharSet.Unicode,

            CallingConvention = CallingConvention.Cdecl,

            EntryPoint = "acedTextBox")]

        public static extern System.IntPtr acedTextBox(

            IntPtr rb,

            double[] point1,

            double[] point2);



        void GetTextBoxCorners(DBText dbText, out Point3d pt1, out Point3d pt2, out Point3d pt3, out Point3d pt4)

        {

            ads_name name = new ads_name();



            int result = acdbGetAdsName(

                ref name,

                dbText.ObjectId);



            ResultBuffer rb = new ResultBuffer();



            Interop.AttachUnmanagedObject(

                rb,

                acdbEntGet(ref name), true);



            double[] point1 = new double[3];

            double[] point2 = new double[3];



            // Call imported arx function

            acedTextBox(rb.UnmanagedObject, point1, point2);



            pt1 = new Point3d(point1);

            pt2 = new Point3d(point2);

            var ptX = pt1 + Vector3d.XAxis * 40;
            var ptY = pt2 + Vector3d.YAxis * 50;


            var lX = new Line(pt1, ptX);
            var lY = new Line(pt2, ptY);

            lX.Color= Color.FromColor(System.Drawing.Color.Green);
            lY.Color= Color.FromColor(System.Drawing.Color.Orange);


            Line line = new Line(pt1, pt2);

            line.Color = Color.FromColor(System.Drawing.Color.Red);

            line.ToSpace();
            lX.ToSpace();
            lY.ToSpace();

            // Create rotation matrix

            Matrix3d rotMat = Matrix3d.Rotation(

                dbText.Rotation,

                dbText.Normal,

                pt1);



            // The returned points from acedTextBox need

            // to be transformed as follow

            pt1 = pt1.TransformBy(rotMat).Add(dbText.Position.GetAsVector());

            pt2 = pt2.TransformBy(rotMat).Add(dbText.Position.GetAsVector());

            Line linetrans = new Line(pt1, pt2);

            linetrans.Color = Color.FromColor(System.Drawing.Color.Yellow) ;

            linetrans.ToSpace();


            Vector3d rotDir = new Vector3d(

                -Math.Sin(dbText.Rotation),

                Math.Cos(dbText.Rotation), 0);


            //求垂直於rotDir和normal的法向量
            Vector3d linDir = rotDir.CrossProduct(dbText.Normal);



            double actualWidth =

                Math.Abs((pt2.GetAsVector() - pt1.GetAsVector())

                    .DotProduct(linDir));



            pt3 = pt1.Add(linDir * actualWidth);

            pt4 = pt2.Subtract(linDir * actualWidth);

            Line linetrans2 = new Line(pt3, pt4);

            linetrans2.Color = Color.FromColor(System.Drawing.Color.Blue);

            linetrans2.ToSpace();
        }

        #endregion
    }

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

023.掌握Pod-Pod擴容和縮容

一 Pod的擴容和縮容

Kubernetes對Pod的擴縮容操作提供了手動和自動兩種模式,手動模式通過執行kubectl scale命令或通過RESTful API對一個Deployment/RC進行Pod副本數量的設置。自動模式則需要用戶根據某個性能指標或者自定義業務指標,並指定Pod副本數量的範圍,系統將自動在這個範圍內根據性能指標的變化進行調整。

1.1 手動縮容和擴容

  1 [root@uk8s-m-01 study]# vi nginx-deployment.yaml
  2 apiVersion: apps/v1beta1
  3 kind: Deployment
  4 metadata:
  5   name: nginx-deployment
  6 spec:
  7   replicas: 3
  8   template:
  9     metadata:
 10       labels:
 11         app: nginx
 12     spec:
 13       containers:
 14       - name: nginx
 15         image: nginx:1.7.9
 16         ports:
 17         - containerPort: 80
  1 [root@uk8s-m-01 study]# kubectl create -f nginx-deployment.yaml
  2 [root@uk8s-m-01 study]# kubectl scale deployment nginx-deployment --replicas=5	#擴容至5個
  3 [root@uk8s-m-01 study]# kubectl get pods	                                	#查看擴容后的Pod

  1 [root@uk8s-m-01 study]# kubectl scale deployment nginx-deployment --replicas=2	#縮容至2個
  2 [root@uk8s-m-01 study]# kubectl get pods

1.2 自動擴容機制

Kubernetes使用Horizontal Pod Autoscaler(HPA)的控制器實現基於CPU使用率進行自動Pod擴縮容的功能。HPA控制器基於Master的kube-controller-manager服務啟動參數–horizontal-pod-autoscaler-sync-period定義的探測周期(默認值為15s),周期性地監測目標Pod的資源性能指標,並與HPA資源對象中的擴縮容條件進行對比,在滿足條件時對Pod副本數量進行調整。

  • HPA原理

Kubernetes中的某個Metrics Server(Heapster或自定義MetricsServer)持續採集所有Pod副本的指標數據。HPA控制器通過Metrics Server的API(Heapster的API或聚合API)獲取這些數據,基於用戶定義的擴縮容規則進行計算,得到目標Pod副本數量。
當目標Pod副本數量與當前副本數量不同時,HPA控制器就向Pod的副本控制器(Deployment、RC或ReplicaSet)發起scale操作,調整Pod的副本數量,完成擴縮容操作。

  • HPA指標類型

Master的kube-controller-manager服務持續監測目標Pod的某種性能指標,以計算是否需要調整副本數量。目前Kubernetes支持的指標類型如下:
Pod資源使用率:Pod級別的性能指標,通常是一個比率值,例如CPU使用率。
Pod自定義指標:Pod級別的性能指標,通常是一個數值,例如接收的請求數量。
Object自定義指標或外部自定義指標:通常是一個數值,需要容器應用以某種方式提供,例如通過HTTP URL“/metrics”提供,或者使用外部服務提供的指標採集URL。
Metrics Server將採集到的Pod性能指標數據通過聚合API(Aggregated API) 如metrics.k8s.io、 custom.metrics.k8s.io和external.metrics.k8s.io提供給HPA控制器進行查詢。

  • 擴縮容算法

Autoscaler控制器從聚合API獲取到Pod性能指標數據之後,基於下面的算法計算出目標Pod副本數量,與當前運行的Pod副本數量進行對比,決定是否需要進行擴縮容操作:
desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]

即當前副本數 x(當前指標值/期望的指標值),將結果向上取整。
釋義:以CPU請求數量為例,如果用戶設置的期望指標值為100m,當前實際使用的指標值為200m,則計算得到期望的Pod副本數量應為兩個(200/100=2)。如果設置的期望指標值為50m,計算結果為0.5,則向上取整值為1, 得到目標Pod副本數量應為1個。
注意:當計算結果與1非常接近時,可以設置一個容忍度讓系統不做擴縮容操作。容忍度通過kube-controller-manager服務的啟動參數–horizontalpod-autoscaler-tolerance進行設置,默認值為0.1(即10%),表示基於上述算法得到的結果在[-10%-+10%]區間內,即[0.9-1.1],控制器都不會進行擴縮容操作
也可以將期望指標值(desiredMetricValue)設置為指標的平均值類型,例如targetAverageValue或targetAverageUtilization,此時當前指標值(currentMetricValue) 的算法為所有Pod副本當前指標值的總和除以Pod副本數量得到的平均值。
此外,存在幾種Pod異常的如下情況:

  • Pod正在被刪除(設置了刪除時間戳):將不會計入目標Pod副本數量。
  • Pod的當前指標值無法獲得:本次探測不會將這個Pod納入目標Pod副本數量,後續的探測會被重新納入計算範圍。
  • 如果指標類型是CPU使用率,則對於正在啟動但是還未達到Ready狀態的Pod,也暫時不會納入目標副本數量範圍。

提示:可以通過kubecontroller-manager服務的啟動參數–horizontal-pod-autoscaler-initialreadiness-delay設置首次探測Pod是否Ready的延時時間,默認值為30s。

另一個啟動參數–horizontal-pod-autoscaler-cpuinitialization-period設置首次採集Pod的CPU使用率的延時時間。
當存在缺失指標的Pod時,系統將更保守地重新計算平均值。系統會假設這些Pod在需要縮容(Scale Down) 時消耗了期望指標值的100%,在需要擴容(Scale Up)時消耗了期望指標值的0%,這樣可以抑制潛在的擴縮容操作。
此外,如果存在未達到Ready狀態的Pod,並且系統原本會在不考慮缺失指標或NotReady的Pod情況下進行擴展,則系統仍然會保守地假設這些Pod消耗期望指標值的0%,從而進一步抑制擴容操作。如果在HorizontalPodAutoscaler中設置了多個指標,系統就會對每個指標都執行上面的算法,在全部結果中以期望副本數的最大值為最終結果。如果這些指標中的任意一個都無法轉換為期望的副本數(例如無法獲取指標的值),系統就會跳過擴縮容操作。
最後, 在HPA控制器執行擴縮容操作之前,系統會記錄擴縮容建議信息(Scale Recommendation)。控制器會在操作時間窗口(時間範圍可以配置)中考慮所有的建議信息,並從中選擇得分最高的建議。這個值可通過kube-controller-manager服務的啟動參數–horizontal-podautoscaler-downscale-stabilization-window進行配置,默認值為5min。這個配置可以讓系統更為平滑地進行縮容操作,從而消除短時間內指標值快速波動產生的影響。

1.3 HorizontalPodAutoscaler

Kubernetes將HorizontalPodAutoscaler資源對象提供給用戶來定義擴縮容的規則。
HorizontalPodAutoscaler資源對象處於Kubernetes的API組“autoscaling”中, 目前包括v1和v2兩個版本。 其中autoscaling/v1僅支持基於CPU使用率的自動擴縮容, autoscaling/v2則用於支持基於任意指標的自動擴縮容配置, 包括基於資源使用率、 Pod指標、 其他指標等類型的指標數據。
示例1:基於autoscaling/v1版本的HorizontalPodAutoscaler配置,僅可以設置CPU使用率。

  1 [root@uk8s-m-01 study]# vi php-apache-autoscaling-v1.yaml
  2 apiVersion: autoscaling/v1
  3 kind: HorizontalPodAutoscaler
  4 metadata:
  5   name: php-apache
  6 spec:
  7   scaleTargetRef:
  8     apiVersion: apps/v1
  9     kind: Deployment
 10     name: php-apache
 11   minReplicas: 1
 12   maxReplicas: 10
 13   targetCPUUtilizationPercentage: 50


釋義:
scaleTargetRef:目標作用對象,可以是Deployment、ReplicationController或ReplicaSet。
targetCPUUtilizationPercentage:期望每個Pod的CPU使用率都為50%,該使用率基於Pod設置的CPU Request值進行計算,例如該值為200m,那麼系統將維持Pod的實際CPU使用值為100m。
minReplicas和maxReplicas:Pod副本數量的最小值和最大值,系統將在這個範圍內進行自動擴縮容操作, 並維持每個Pod的CPU使用率為50%。
為了使用autoscaling/v1版本的HorizontalPodAutoscaler,需要預先安裝Heapster組件或Metrics Server,用於採集Pod的CPU使用率。
示例2:基於autoscaling/v2beta2的HorizontalPodAutoscaler配置。

  1 [root@uk8s-m-01 study]# vi php-apache-autoscaling-v2.yaml
  2 apiVersion: autoscaling/v2beta2
  3 kind: HorizontalPodAutoscaler
  4 metadata:
  5   name: php-apache
  6 spec:
  7   scaleTargetRef:
  8     apiVersion: apps/v1
  9     kind: Deployment
 10     name: php-apache
 11   minReplicas: 1
 12   maxReplicas: 10
 13   metrics:
 14   - type: Resource
 15     resource:
 16       name: cpu
 17       target:
 18         type: Utilization
 19         averageUtilization: 50


釋義:
scaleTargetRef:目標作用對象,可以是Deployment、ReplicationController或ReplicaSet。
minReplicas和maxReplicas:Pod副本數量的最小值和最大值,系統將在這個範圍內進行自動擴縮容操作, 並維持每個Pod的CPU使用率為50%。
metrics:目標指標值。在metrics中通過參數type定義指標的類型;通過參數target定義相應的指標目標值,系統將在指標數據達到目標值時(考慮容忍度的區間)觸發擴縮容操作。

  • metrics中的type(指標類型)設置為以下幾種:
    • Resource:基於資源的指標值,可以設置的資源為CPU和內存。
    • Pods:基於Pod的指標,系統將對全部Pod副本的指標值進行平均值計算。
    • Object:基於某種資源對象(如Ingress)的指標或應用系統的任意自定義指標。

Resource類型的指標可以設置CPU和內存。對於CPU使用率,在target參數中設置averageUtilization定義目標平均CPU使用率。對於內存資源,在target參數中設置AverageValue定義目標平均內存使用值。指標數據可以通過API“metrics.k8s.io”進行查詢,要求預先啟動Metrics Server服務。
Pods類型和Object類型都屬於自定義指標類型,指標的數據通常需要搭建自定義Metrics Server和監控工具進行採集和處理。指標數據可以通過API“custom.metrics.k8s.io”進行查詢,要求預先啟動自定義Metrics Server服務。
類型為Pods的指標數據來源於Pod對象本身, 其target指標類型只能使用AverageValue,示例:

  1  metrics:
  2   - type: Pods
  3     pods:
  4       metrics:
  5         name: packets-per-second
  6       target:
  7         type: AverageValue
  8         averageValue: 1k

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

HttpClient在高併發場景下的優化實戰

在項目中使用HttpClient可能是很普遍,尤其在當下微服務大火形勢下,如果服務之間是http調用就少不了跟http客戶端找交道.由於項目用戶規模不同以及應用場景不同,很多時候可能不需要特別處理也.然而在一些高併發場景下必須要做一些優化.

項目是快遞公司的快件軌跡查詢項目,目前平均每小時調用量千萬級別.軌跡查詢以Oracle為主要數據源,Mongodb為備用,當Oracle不可用時,數據源切換到Mongodb.今年菜鳥團隊加入后,主要數據遷移到了阿里雲上,以Hbase為主要存儲.其中Hbase數據查詢服務由數據解析組以Http方式提供.原有Mongodb棄用,雲上數據源變為主數據源,Oracle作為備用.當數據源切換以後,主要的調用方式也就變成了http方式.在第10月初第一輪雙11壓測試跑上,qps不達標.當然這個問題很好定位,因為十一假之間軌跡域組內已經進行過試跑,當時查的是oracle.十一假期回來后,只有這一處明顯的改動,很容易定位到問題出現在調用上.但具體是雲上Hbase慢,還是網絡傳輸問題(Hbase是阿里雲上的服務,軌跡查詢項目部署在IDC機房).通過雲服務,解析組和網絡運維的配合,確定問題出現在應用程序上.在Http服務調用處打日誌記錄,發現以下問題:

可以看到每隔一段時間,就會有不少請求的耗時明顯比其它的要高.

導致這種情況可能可能是HttpClient反覆創建銷毀造成引起來銷,首先憑經驗可能是對HttpClient進行了Dispose操作(Using(HttpClient client=new HttpClient){…})

如果你裝了一些第三方插件,當你寫的HttpClient沒有被Using包圍的時候會給出重構建議,建議加上Using或者手動dispose.然而實際中是否要dispose還要視情況而定,對於一般項目大家的感覺可能是不加也沒有大問題,加了也還ok.但是實際項目中,一般不建議反覆重新創建這個對象,關於HttpClient是否需要Dispose請看

在對這個問題的答案里,提問者指出微軟的一些示例也是沒有使用using的.下面一個比較熱的回答指出HttpClient生命周期應該和應用程序生命周期一致,只要應用程序需要Http請求,就不應用把它Dispose掉.下面的一個仍然相對比較熱的回答指出一般地,帶有Dispose方法的對象都應當被dispose掉,但是HttpClient是一個例外.

當然以上只是結合自己的經驗對一個大家都可能比較困惑的問題給出一個建議,實際上對於一般的項目,用還是不用Dispose都不會造成很大問題.本文中上面提到的問題跟HttpClient也沒有關係,因為程序中使用的Http客戶端是基於HttpWebRequest封裝的.

問題排查及優化

經過查詢相關資料以及同行的經驗分享(給了很大啟發)

查看代碼,request.KeepAlive = true;查詢,這個屬性其實是設置一個’Keep-alive’請求header,當時同事封裝Http客戶端的場景現場無從得知,然而對於本文中提到的場景,由於每次請求的都是同一個接口,因此保持持續連接顯然能夠減少反覆創建tcp連接的開銷.因此註釋掉這一行再發布測試,以上問題便不復出現了!

當然實際中做的優化絕不僅僅是這一點,如果僅僅是這樣,一句話就能夠說完了,大家都記住以後就這樣做就Ok了.實際上還參考了不少大家在實際項目中的經驗或者坑.下面把整個HttpClient代碼貼出來,下面再對關鍵部分進行說明.

 public static string Request(string requestUrl, string requestData, out bool isSuccess, string contentType = "application/x-www-form-urlencoded;charset=utf8")
    {
        string apiResult = "";
        isSuccess = false;
        if (string.IsNullOrEmpty(requestData))
        {
            return apiResult;
        }
        HttpWebRequest request = null;
        HttpWebResponse response = null;
        try
        {
            byte[] buffer = Encoding.UTF8.GetBytes(requestData);
            request = WebRequest.Create($"{requestUrl}") as HttpWebRequest;
            request.ContentType = "application/json";
            request.Method = "POST";
            request.ContentLength = buffer.Length;
            request.Timeout =200;
            request.ReadWriteTimeout =  Const.HttpClientReadWriteTimeout
            request.ServicePoint.Expect100Continue = false;
            request.ServicePoint.UseNagleAlgorithm = false;
            request.ServicePoint.ConnectionLimit = 2000
            request.AllowWriteStreamBuffering = false;
            request.Proxy = null;

            using (var stream = request.GetRequestStream())
            {
                stream.Write(buffer, 0, buffer.Length);
            }
            using (response = (HttpWebResponse)request.GetResponse())
            {
                string encoding = response.ContentEncoding;
                using (var stream = response.GetResponseStream())
                {
                    if (string.IsNullOrEmpty(encoding) || encoding.Length < 1)
                    {
                        encoding = "UTF-8"; //默認編碼
                    }

                    if (stream != null)
                    {
                        using (StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(encoding)))
                        {
                            apiResult = reader.ReadToEnd();
                            //byte[] bty = stream.ReadBytes();
                            //apiResult = Encoding.UTF8.GetString(bty);
                        }
                    }
                    else
                    {
                        throw new Exception("響應流為null!");
                    }
                }
            }
            isSuccess = true;
        }
        catch (Exception err)
        {
            isSuccess = false;
            LogUtilities.WriteException(err);
        }
        finally
        {
            response?.Close();
            request?.Abort();
        }

        return apiResult;
    }
  • 首先是TimeOut問題,不僅僅是在高併發場景下,實際項目中建議不管是任何場景都要設置它的值.在HttpWebRequest對象中,它的默認值是100000毫秒,也就是100秒.如果服務端出現問題,默認設置將會造成嚴重阻塞,對於普通項目也會嚴重影響用戶體驗.返回失敗讓用戶重試也比這樣長時間等待體驗要好.

  • ReadWriteTimeout很多朋友可能沒有接觸過這個屬性,尤其是使用.net 4.5里HttpClient對象的朋友.有過Socket編程經驗的朋友可能會知道,socket連接有連接超時時間和傳輸超時時間,這裏的ReadWriteTimeout類似於Socket編程里的傳輸超時時間.從字面意思上看,就是讀寫超時時間,防止數據量過大或者網絡問題導致流傳入很長時間都無法完成.當然在一般場景下大家可以完全不理會它,如果由於網絡原因造成讀寫超時也很有可能造成連接超時.這裏之所以設置這個值是由於實際業務場景決定的.大家可能已經看到,以上代碼對於ReadWriteTimeout的設置並不像Timeout一樣設置為一個固定值,而是放在了一個Const類中,它實際上是讀取一個配置,根據配置動態決定值的大小.實際中的場景是這樣的,由於壓測環境無法完全模擬真實的用戶訪問場景.壓測的時候都是使用單個單號進行軌跡查詢,但是實際業務中允許用戶一次請求查詢最多多達數百個單號.一個單號的軌跡記錄一般都是幾十KB大小,如果請求的單號數量過多數量量就會極大增加長,查詢時間和傳輸時間都會極大增加,為了保證雙11期間大多數用戶能正常訪問,必要時會把這個時間設置的很小(默認是3000毫秒),讓單次查詢量大的用戶快速失敗.

以上只是一種備用方案,不得不承認,既然系統允許一次查詢多個單號,因此在用戶在沒有達到上限之前所有的請求都是合法的,也是應該予以支持的,因此以上做法實際上有損用戶體驗的,然而系統的資源是有限的,要必要的時候只能犧牲特殊用戶的利益,保證絕大多數用戶的利益.雙11已經渡過,實際中雙11也沒有改動以上配置的值,但是做為風險防範增加動態配置是必要的.

這裏再多差一下嘴,就是關於ContentLength它的值是可以不設置的,不設置時程序會自動計算,但是設置的時候一定要設置字節數組的長度,而不是字符串的長度,因為包含中文時,根據編碼規則的不同,中文字符可能佔用兩個字節或者更長字節長度.

  • 關於 request.ServicePoint.Expect100Continue = false; request.ServicePoint.UseNagleAlgorithm = false;這兩項筆者也不是特別清楚,看了相關文檔也沒有特別明白,還請了解的朋友指點,大家共同學習進步.

  • request.ServicePoint.ConnectionLimit = 2000是設置最大的連接數,不少朋友是把這個數值設置為65536,實際上單台服務器web併發連接遠太不到這個數值.這裏根據項目的實際情況,設置為2000.以防止處理能力不足時,請求隊列過長.

  • request.AllowWriteStreamBuffering = false;根據[微軟文檔()]這個選項設置為true時,數據將緩衝到內存中,以便在重定向或身份驗證請求時可以重新發送數據.

最為重要的是,文檔中說將 AllowWriteStreamBuffering 設置為 true 可能會在上傳大型數據集時導致性能問題,因為數據緩衝區可能會使用所有可用內存。由於發送的請求僅僅是單號,數據量很小,並且很少有用戶一個單號反覆查詢的需求.加上可能會有副作用.這裏設置為false.

  • request.Proxy = null;這裡是參考了一個一位網友的文章,裏面提到默認的Proxy導致超時怪異行為.由於解決問題是在10月份,據寫這篇文章已經有一段時間了,因此再尋找時便找不到這篇文章了.有興趣的朋友可以自己搜索一下.

很多朋友可能會關心,通過以上配置到底有沒有解決問題.實際中以上配置后已經經歷了雙11峰值qps過萬的考驗.下面給出寫本文時候請求耗時的監控

可以看到,整體上請求耗時比較平穩.

可能看了這個圖,有些朋友還是會有疑問,通過上面日誌截圖可以看到,除了耗時在100ms以上的請求外,普通的耗時在四五十毫秒的還是有很多的,但是下面這個截圖裡都是在10到20區間浮動,最高的也就30ms.這其實是由於在壓測的過程中,發現Hbase本身也有不穩定的因素(大部分請求響應耗時都很平穩,但是偶爾會有個別請求婁千甚至數萬毫秒(在監控圖上表現為一個很突兀的線,一般習慣稱為毛刺),這在高併發場景下是不能接受的,問題反饋以後阿里雲對Hbase進行了優化,優化以後耗時也有所下降.)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

《軟件方法(上)》讀書筆記

1、建模

1.1、業務建模之願景

重點1:通俗一點講,一個東西的願景就是:東西最應該賣個誰,對他有什麼好處?

重點2:願景是需求排序的主要依據。

重點3:老大、願景、需求都是基於現狀尋找最值得的改進。改進過後,又是新的現狀了,還是基於現狀尋找最值得的改進。進一步說也可以說,需求只有真假對錯,沒有變化。說需求有變化,那是從一個靜止時間點來看的。

1.1.1、願景




建模之願景

1.2、業務建模之業務用例圖

有了願景,我們知道老大對他所代表的組織的現狀的某些指標不滿意。接下來就可以研究組織,弄清楚到底是組織的哪些環節造成了這些指標比較差,這就是業務建模(Business modeling)的主要內容。

重點1:軟件系統只是組織的一個零件。組織裏面還有很多系統,其中最值錢的是千百年來一直在使用,現在依然是最複雜的系統——人腦系統。

重點2:開發團隊發現需求“容易變化”。根源之一是需求的來路不正,沒有把系統當作一個零件放在組織中來看,靠拍腦袋得出需求,導致得到的系統需求是錯的。

1.2.1、業務角色

① 業務執行者

以某組織為研究對象,在組織之外和組織交互的其他組織(人群或機構)就是該組織的執行者。以一家商業銀行為研究對象,觀察在它邊界之外和它打交道的人群或機構,可以看到儲戶來存錢,企業來貸款,人民銀行要它作監督等等,這些就是該商業銀行的執行者,如下圖所示:




業務執行者(一)

這裏要注意的是,作為觀察者的建模人員本身是一個人腦系統,所以在觀察組織邊界時,直覺上觀察到的不是組織之間的交互,而是組織派出的系統之間的交互,但是一定要把它理解成組織間的交互,因為談論業務執行者時,研究對象是組織,所以外部對應物——業務執行者也應該是組織。例如:以某國稅局為研究對象,可以觀察到企業財務人員到國稅局報稅,但業務執行者不是企業財務人員,而是企業。也許到後來,企業財務人員和國稅系統交互,又或許再後來是企業系統與國稅系統交互,從組織的抽象級別來看,都應該理解為企業和國稅局這兩個機構之間的交互,如下圖所示:




業務執行者(二)

② 業務工人

組織內的人稱為業務工人,例如某商業銀行裏面的營業員。業務工人是可以被替換的人腦零件,它可能被其他業務工人替換,但更有可能被業務實體替換。

③ 業務實體

業務實體是組織中的非人智能系統,例如銀行的ATM、點鈔機、營業系統。

1.2.2、識別業務用例

重點1:業務用例指業務執行者希望通過和所研究組織交互獲得的價值。業務用例是組織的價值,不會因為某個人腦系統或電腦系統的存在或消失而改變,好比如300年前的商業銀行和當前的銀行的業務用例是不變的,因為銀行提供價值的本質沒有改變。所以“這個系統的業務用例是什麼”這樣的說法是錯誤的。

重點1:用好用例,關鍵在於理解“價值”。價值是期望和承諾的平衡點、買賣的平衡點。例如以“醫院”為研究對象,真正的用例是“患者→看病”,而不是“患者→挂號”,患者→挂號”可以是以“挂號室”為研究對象的業務用例(如下圖)。所以做任何事情之前,要搞清楚“邊界”,沒有邊界會很容易盲目“拍腦袋”做一些努力但沒效果的事情。




業務用例識別

1.2.2.1、識別業務用例思路

識別業務用例的思路有兩條:

【從外到內】從業務執行者開始考慮,思考業務執行者和組織交互的目的(主要);

【從內到外】通過觀察組織的內部活動,一直問為什麼,向外推導出組織外部的某個業務執行者(補漏)。




業務用例識別思路

1.2.2.1.1、識別業務用例常犯錯誤

錯誤1 :把業務工人的行為當做業務用例。




錯誤用例識別(1)

錯誤2:業務用例隨待引入系統伸縮。




錯誤用例識別(2)

錯誤3:把害怕遺漏掉的擴展路徑片段提升為業務用例。




錯誤用例識別(3)

錯誤4:管理型業務用例。




錯誤用例識別(4)

總結:錯誤的根源主要源於:建模人員分不清問題和問題的解決方案。

1.3、業務建模之業務序列圖

1.3.1、描述業務流程的手段

本章節主要討論的是業務建模中最繁重的工作——描述業務用例的實現,即業務流程,然後改進它,推導出待引入系統的用例。目前描述業務流程的可選擇手段有文本、活動圖和序列圖,它們的主要區別如下(以財務部“員工→報銷”用例的實現為樣例):

● 文本




文本樣例

文本的缺點是不夠生動,所以在描述業務流程時很少使用文本方式。不過,描述系統用例(即系統需求)的流程時,文本是常用的,因為此時更注重精確,而且還要表達業務規則、性能等目前尚未被UML標準覆蓋的內容。

● 活動圖




活動圖樣例

序列圖是UML圖形描述業務流程的兩種選擇之一。活動圖的前身是流程圖,應該是在建模人員中使用頻率最高的圖形,是隨机械工程領域慢慢引入到計算機領域。不過,隨着編程語言表達能力越來越強,針對簡單的分支或循環邏輯畫圖在很多情況下已經變得沒有必要。

● 序列圖




序列圖樣例

序列圖與活動圖的比較如下:

1)活動圖只關注人,序列圖把人當作系統;

在上一章節中已經提到,現在的業務流程中已經有很多領域邏輯是封裝在業務實體而不是業務工人中,如果忽略非人智能系統,很多重要信息就丟掉了。

2)活動圖表示動作,序列圖強迫思考動作背後的目的;

序列圖可以更加清晰地表述業務工人或業務實體對外的責任,也就是用例的期望值。期望和承諾是用例和對象技術的關鍵思想,使用序列圖來做業務建模,“對象協作以完成用例”的思想就可以統一地慣竊業務建模和系統建模的始終。

3)活動圖“靈活”,序列圖“不靈活”;

不少人認為活動圖勝過序列圖的地方是它靈活,但這靈活是一把雙刃劍。活動圖很靈活,他的控制箭頭可以指向任何地方,就像編碼原始時代的“goto”語句,所以活動圖很容易畫。不過,“很容易畫”的活動圖也比較容易掩蓋建模人員對業務流程認識不足或者業務流程本身存在缺陷的事實。序列圖可通過alt、loop等結構化控製片段來描述業務流程,強迫建模人員用這種方式思考。

1.3.2、業務序列圖要點

1.3.2.1、消息代表責任分配而不是數據流動

序列圖中最重要的要點是消息的含義。A指向B的消息,代表“A請求B做某事”,或者“A調用B做某事的服務”,而“做某事”是B的一個責任。




序列圖消息含義

1.3.2.2、抽象級別是系統之間的協作

業務建模的研究對象是組織,出現在業務序列圖生命線上的對象,其最小顆粒是系統,包括人和非人系統,而系統之間最主要突出的是協作。所以“系統粒度”和“協作”是業務序列圖的關鍵要點,如果記住了這兩個關鍵點,就可以避免了對組織對象抽象的錯誤以及對協作理解的錯誤。




系統內部的組件暴露



表達了過細的交互步驟

以上說的兩種錯誤是把需求和分析的工作流的工作帶入了業務建模。第一樣例圖提到的系統內部的組件,應該在分析和設計工作流中描述;第二樣例圖提到了交互步驟,應該在需求工作流中描述。除了以上兩種抽象級別的錯誤,還有一種是:業務序列圖的內容和業務用例圖差不多,如下所示:




目標組織作為整體出現在業務序列圖中  

1.3.2.3、把時間看作特殊的業務實體

業務序列圖中,我們把時間看作特殊的業務實體。把時間看作上帝造好掛在天上的一個大鐘,向全世界各種系統發送時間消息,這樣,就和後面需要工作流中映射系統用例的時間執行者一致了,同時也幫助理清什麼情況下使用時間執行者的問題。




把時間當作一個系統

值得注意的一點是,時間和定時器不是一個概念,時間是“外系統”,定時器是其他系統用來和時間打交道的“邊界類”。世界上只有一個時間系統,但有無數的定時器。如果建模人員在識別系統用例時說“執行者是定時器”,那就錯了,執行者是時間。




時間和定時器的區別

1.3.2.4、為業務對象分配合適的責任

分配給業務對象的責任必須是該對象有能力承擔的,這需要我們自身必須對理解要十分清晰,畢竟我們自己說話有時候的會含糊。例如“工作人員用Word寫標書”這樣的說法好像可以接受,但如果按照說話的文字不假思索地隨便畫,會很容易導致對象責任分配不準確。




不恰當與不恰當的責任分配對比

1.3.3、現狀業務序列圖

業務序列圖描述的是業務流程,建模需要通過在現狀的業務序列圖基礎上找出改進的要點。如果要把現狀序列圖畫出來,就必須讓自己站在客觀的角度“親臨現場”,“如實”地把所看到的記錄下來,儘力描繪出真實的現狀。但說起來非常簡單,做到卻極其困難。總結到這裏,忽然讓我想起了彼得·德魯克。下面列出一些描述現狀時經常犯的錯誤。

1)錯誤:把想象中的改進當成現狀

很多時候造成這種錯誤,背後的原因很可能是根本沒有深入到組織流程中去做觀察和訪談,對現狀沒有認識,只好想像一個改進后的場景來應付。

2)錯誤:把“現狀”誤解為“純手工”

有的建模人員以為人做的事情才是本質,所以他畫的業務流程中,只有人,沒有非人系統,完全忽略了在技術進步下慢慢可以替代人的這些“業務實體”。

3)錯誤:把“現狀”誤解為“本開發團隊未參与之前”

開發團隊很容易會誤以為當他們開始參与組織流程完善而開發系統的時候當作“現狀”,這就是典型的“技術思維”,很多時候在開發團隊在參与組織流程完善前,組織已經經過了許多次“非系統級”的流程改進,這是站在組織角度去看問題的。

4)錯誤:把“現狀”誤解為“規範”

建模人員在建模業務流程時,照搬組織制定的規範,沒有去觀察實際工作中人們是如何做的,或者即使觀察到了人們實際沒有按照規範做,卻依然按照規範建模。這樣做,得到的業務流程是不真實的,畢竟上有政策,下有對策。

5)錯誤:“我是創新,沒有現狀”

互聯網創業公司的建模人員很容易犯的這個錯誤,動不動就說“我做的是互聯網創新,沒有現狀”,但他們已經忘記了歷史上所有的創新都是站在前輩這些巨人的肩膀之上這個事實。

6)錯誤:“我做產品,沒有現狀”

非定製系統的開發團隊進程拿這句話做接口。A公司的流程和B公司的流程有差異,中國的流程和外國的流程有差異,畫誰的現狀好的?問這個問題的時候,我想是開始開發團隊忘記了“做需求時把產品當項目做”的道理,在第2章節中也提到過“誰比誰更像”的重點。

1.3.4、改進業務序列圖

上面提到的現狀業務序列圖是對組織現狀的客觀描述,而改進業務序列圖是通過信息化手段去思考對業務現狀序列圖的一些改進。通常,信息化給人類的工作和生活帶來的改進有三種模式。

1.3.4.1、改進模式一:物流變成信息流

和信息的光電運輸比起來,用其他手段運輸的物的流轉速度就顯得太慢了,而且運輸成本會隨着距離的增加而明顯增加。如果同類物的不同實例之間可以相互取代,那麼可以提煉物中包含的部分或全部有價值的信息,在需要發生物流的地方,改為通過軟件系統交互信息,需要物的時候再將信息變成物,這樣就可以大大增加流轉速度和降低流轉成本。




物流變成信息流改進

1.3.4.2、改進模式二:改善信息流轉

軟件系統越來越多,而各個軟件系統之間溝通不暢,導致一個人為了達到某個目的可能需要和多個軟件系統打交道,如果把各軟件系統之間的協調工作改為一個軟件系統來完成,人只需要和單個軟件系統打交道,信息的流轉就改進了。




改善信息流轉

1.3.4.3、改進模式三:封裝領域邏輯

在業務流程中,有很多步驟是由人腦來判斷和計算的,領域邏輯封裝在人腦中。相對於計算機,人腦(業務人才)存在成本高,狀態不穩定、會徇私舞弊等問題。如果能夠提煉人腦中封裝的領域邏輯,改為封裝到軟件系統中,用軟件系統代替人腦,業務流程就得到了改進。換句話說,領域邏輯的封裝是對系統“內在”價值的提升,相對於前兩個改進模式有更高的要求和更大的困難。




封裝領域邏輯改進

1.3.4.4、改進思考方式:阿布爾思考法

在軟件開發團隊中,當有人提出新的想法時,經常會被馬上否定“太難了,做不了”,最終得到一個平庸的、毫無競爭力的系統。學會像阿布(俄羅斯大富豪羅曼·阿布拉莫維奇)一樣思考,有助於克服普通人因資源受限而不敢展開想象的思維障礙。阿布思考法分兩步:

1)假設有充足的資源去解決問題,得到一個完美的方案;

2)用手上現有的資源去山寨這個完美的方案。

其實,阿布思考法的核心思想就是,不要閉門造車,要“接地氣”的行動起來,主動去觀察,或主動尋找有用的信息去分析和調研,不要被各種局限被迫讓步。

2、需求

2.1、需求之系統用例圖

在“建模”階段我們研究和思考的對象是組織,從組織的整體性客觀地去發現組織如何可以通過信息化手段去優化流程。有了客觀的整體性分析和改進認知,接下來的“需求”階段需要深入到系統層面去思考了。按正常邏輯,每一步都有“承上啟下”的作用,本章節所研究的系統用例就是通過上一步業務序列圖中所映射出來的。

執行者和用例的概念在業務建模的學習中已經出現過,現在要研究的執行者和用例與業務建模時研究的執行者和用例相比,不同之處是研究對象,之前研究的是組織,現在研究的是系統。

2.1.1、系統執行者要點

系統執行者的定義:在所研究系統外,與該系統發生功能性交互的其他系統。

2.1.1.1、系統是能獨立對外提供服務的整體

封裝了自身的數據和行為,能獨立對外提供服務的東西才能稱為系統。不了解這點,建模人員很容易把“添加一些功能”當作“研發新系統”。




劃分系統用例

2.1.1.2、系統邊界是責任的邊界

系統執行者不是所研究系統的一部分,是該系統邊界外的另一個系統。這裏的系統邊界不是物理的邊界,而是責任的邊界。




錯誤的遙控軟件用例圖    



正確的遙控軟件用例圖

 

2.1.1.3、系統執行者和系統有交互

外系統必須和系統有交互,否則不能算是系統的執行者。如一名旅客來到火車站售票窗口,告訴售票員目的地和車次,售票員使用售票系統幫助旅客購買火車票,這個場景中,和火車票系統交互的是售票員,他是售票系統的執行者。




旅客不是售票系統的執行者

如果火車售票系統現在已經提供了旅客自行購票的接口,例如互聯網購票、售票機等,這種情況下,旅客也是售票系統的執行者。不過“售票員→售票”和“旅客→購票”是兩個不同的用例。

2.1.1.4、交互是功能性交互

上面說的交互還引出一個問題:假設售票員使用鼠標和售票系統交互,按道理,比起銷售員,鼠標里售票系統更近,為什麼不把鼠標作為售票系統的執行者呢?還有,假設售票系統運行在Windows操作系統之上,那麼Windows是不是售票系統的執行者?其實吧,辨別這些問題的要點就是:執行者和系統發生的交互是系統的功能需求。鼠標和操作系統跟售票系統的交互都不是售票系統的核心域概念。售票員和售票系統之間的交互才是,所以售票員才是售票系統的執行者。




售票系統的功能需求

2.1.1.5、系統執行者可以是人或非人系統

系統執行者可以是一個人腦系統,也可以是一個非人智能系統,甚至是一個特別的系統——時間。在軟件業的早期,一個系統的執行者往往全部都是人。隨着時間的推移,系統的執行者中非人執行者所佔的比例越來越多。用例的優勢在於“執行者”和“涉眾”的概念,把演員和觀眾分開。演員(執行者)在台上表演,觀眾(涉眾)在台下看,演員表演什麼是由觀眾的口味決定的,演員可以不是人,但觀眾肯定是人。演員如果是人類,那麼在觀眾席上也會有一個位置,不過在第幾排就不知道了(權重等級)。用例使用“執行者”和“涉眾”代替了原來的“用戶”是一個非常大的突破,建模人員如果過多地關注“用戶”,混淆了真正真正重要的前排“涉眾”的需求,把操作人員當前重要的調研對象(非關鍵人員),那麼花在重要的前排涉眾(關鍵人)身上的時間可能就不夠了。越來越多的系統執行者不是人類,也就是說沒有“用戶”。




從“執行者都是人”到“執行者有一些是人”

 

2.1.1、識別系統執行者

2.1.2.1、從業務序列圖映射系統執行者

如果沒有做業務建模,識別系統執行者只能靠頭腦風暴。例如:什麼人會使用系統來工作?什麼人負責維護系統?系統需要和哪些其他智能系統交互?有沒有定時引發的事件?等等問題。有了業務建模,可以直接從業務序列圖映射即可。業務序列圖上,和所研究系統有實線相連的對象映射為所研究系統的執行者。




業務序列圖:需找租客線索  



從業務序列圖映射得到系統執行者

 

2.1.3、系統用例要點

2.1.3.1、價值的買賣的平衡點

系統用例的定義:系統能夠為執行者提供的、涉眾可以接受的價值。和業務用例相比,研究對象從組織變成了系統,要理解好系統用例,重點依然是之前所強調的買賣平衡點、期望和承諾平衡點。

用例之前的許多需求方法學,把需求定義為思考系統“做什麼”,用例把需求提升到思考系統“賣什麼”的高度。這種思考是非常艱難的,因為它沒有標準答案,只有最佳答案。要得到這個答案,不能靠拍腦袋,必須揣摩涉眾。要得到合適的用例,需要有一顆善於提擦他人的心。




ATM和程序員人腦系統的用例

2.1.3.2、價值不等於可以這樣做

有些人會較真,還是以ATM為例子,有些人會因為“難道ATM放在那裡我就不能登錄一下就離開嗎?我今晚下班就去ATM那裡登錄一下給你看,然後走人。”ATM確實能登錄,但登錄功能並非ATM的賣點,如果以一個“門禁系統”為研究對象,登錄就可以作為它的用例。

還有一種情況,例如科員可以有A和B用例,科長因為比科員的權力大,所以就能擁有科員的用例。用例的執行者只是表明這個用例是為這一類執行者而做的。但不代表系統一定要有權限控制以防止其他的人或電腦系統使用該用例。即時系統確實需要有權限控制,而且角色的劃分和執行者相近,也要把這兩者分開,更不可以因為系統不設權限控制,所以把執行者的名字合併為“用戶”。




用例劃分案例

有些書中會給出“最佳粒度原則”。例如:一個系統的用例最好控制在XXX個之內,一個用例的基本路徑最好控制在X步到X步之間……這些是沒有根據的。市場需要各種各樣的系統,有功能眾多的,也有功能單一的,也有交付複雜的,應該把屁股坐到涉眾那邊去,揣摩涉眾的心裏,實事求是地寫下來。如果建模人員在粒度問題上激烈爭吵以及糾纏不清,有可能已經犯了錯誤,最常犯的錯誤是把步驟當作用例。




錯誤:把步驟當作用例

2.1.3.3、增刪改查用例的根源是從設計映射需求

有一些用例圖,映入眼帘的用例是四個四個一組的,仔細一看,剛好對應看數據庫的四種操作。相當於把數據庫的各個表名加上新增、刪除、修改、查詢,就得到了用例的名字。有些建模人員確實也知道這個錯誤,但他們學乖了,乾脆把每四個用例合併,改名叫“管理XX”或(“XX管理”),然後新增、刪除、修改、查詢等用例再擴展它,可惜依然是換湯不換藥。




從數據庫視角得到的用例

 

2.1.3.4、從設計映射需求錯誤二:“復用”用例

增刪改查用例實際上就是從設計映射需求,導致“復用”用例的一種情況。在看看以下例子:




“復用”用例錯誤示例——缺陷管理系統

從不同的業務序列圖分別映射得到系統有右邊四個用例,但有的建模人員會動起心思:這些實現起來不都是針對“缺陷”表來“select X X X from缺陷表where X X X”嗎,合併成一個用例“查詢缺陷”多好!於是得到左邊的結果。實際上,右邊這四個用例面對的執行者不同,背後的涉眾利益也有差別。

當然,如果真的像這位建模人員講的,把“數據庫”,買回去就好,想怎麼折騰這信息都可以那不是更加簡單。其實,用例是涉眾願意“購買”的、對系統的一種“用法”,只要涉眾願意“購買”,當然越多越好。講到這裏,就要來說一個需求的基本要點:需求不考慮“復用”,如果考慮“復用”,要警惕自己是不是已經轉換到了設計視角來思考問題。

2.1.3.5、系統用例不存在層次問題

系統用例的研究對象就是某特定系統,不是組織,也不是系統內部的組件。如果存在“層次”上的疑惑,背後的原因是研究對象不知不覺改變了。

像醫院信息系統的用例,有人會畫成如下圖所示,原因可能是前面沒有畫業務用例圖和業務序列圖,所以建模人員頭腦里不知不覺把醫院信息系統的價值和醫院的價值混在一起了。




錯誤的“高層”用例:混淆組織的價值和系統的價值

還有以下的防汛系統用例圖,把系統的願景當成了“高層”用例:




錯誤的“高層”用例:把願景當作用例

以下更為常見的錯誤,為系統的“模塊”或“子系統”畫用例圖:




錯誤:模塊的用例  



用例仍然是系統的用例    



錯誤:子系統的用例

2.1.3.6、用例的命名是動賓結構

用例的命名是動賓結構,例如“取現金”。動詞前面可以加狀語,賓語前面可以加定語,把一句話的主語砍掉,剩下的可以用作用例的名字。

給用例起名時不要使用弱動詞。用例之前的需求技術,可能是以“名詞+動詞”的形式命名系統的功能,例如“發票作廢”,後來要改成用例的動賓結構了,有的建模人員就在前面加一個弱動詞“進行”,就變成了“進行發票作廢”,這個也是不合適的。

如果“名詞+動詞”已經成為行業中的一個術語,也未必要嚴格的動賓結構,例如“成果分析”是某行業的一個術語,也就不必硬要倒過來變成“分析成果”了。

2.1.4、識別系統用例

2.1.4.1、從業務序列圖映射系統用例

其實,只要認真做好業務建模,從業務序列圖上映射系統用例,得到的結果自然就會符合上面說的這些要點。

從業務序列圖中,從外部指向所研究系統的消息,可以映射為該系統的用例。現在我們繼續從“識別系統執行者”的用例中結合執行者和系統用例一起識別。




從業務序列圖上找到從外部指向所研究系統的信息    



從業務序列圖映射得到系統用例

 

在以上業務序列圖中,有一處消息是“外呼人員”指向“線索管理系統”的消息為“提供本人當天名單”,但在以上系統用例圖中,用例名改為了“查看本人當天名單”。因為序列圖上的消息代表“請求某系統做某事”,用例代表“用某系統來做某事”,一定要理解兩種圖的要點,所以有的地方需要調整。

在以上系統用例圖中,有的箭頭是從執行者指向用例,這樣的執行者稱為用例的主執行者,有的箭頭是從用例指向執行者,這樣的執行者稱為用例的輔執行者。主執行者主動發起用例的交互,輔執行者在交互的過程中被動參与進來。

值得注意一下,輔執行者這個概念是被誤用的比較多。最常見的錯誤是把信息的接收者或者將來可能使用信息的人當成輔執行者。另一種輔執行者的誤用剛好相反,把信息的來源當作輔執行者。




錯誤:把可能會用到所生產信息的人當作輔執行者  



錯誤:把提供用例所需要信息的人當作輔執行者

以上錯誤的原因很多是因為前面沒有畫業務序列圖,導致建模人員在畫系統用例圖的時候產生焦慮,總是希望在圖上多放一些信息,以免自己忘記了。一般來說,輔執行者是非人智能系統的情況較多,人腦系統作為輔執行者的情況比較少,所以碰到輔執行者是人的時候,要多留心。




正確:合適的輔執行者(因為辦卡需要用戶輸入密碼)

 

2.2、需求之系統用例規約

用例圖表達了用例的目標,但是對於完整的需求來說,這是遠遠不夠的。用例的背後封裝了不同級別的相關需求,我們需要通過書寫用例規約把這些需求表達出來。用例規約就是以用例為核心來組織需求內容的需求規約。用例規約的各項內容可以通過以下類圖來展示:




用例規約的內容

2.2.1、前置條件和後置條件

用例通過前置條件(precondition)、後置條件(postcondition)以契約的形式表達需求。用例相當於系統的一個承諾:在滿足前置條件時開始,按照裏面的路徑步驟走,系統就能達到後置條件。為了避免掉入“從實現角度看這樣可以那樣也可以”的陷阱,後置條件只需要寫出最想要的那個狀態即可。

● 前置條件:用例開始前,系統需要滿足的約束。

● 後置條件:用例成功結束后,系統需要滿足的約束。

2.2.1.1、前置條件、後置條件必須是系統能檢測的




系統必須能檢測前置、後置條件

以上圖為例,“錄入保單”用例的前置條件是錯誤的。業務代表是否已經把保單交給內勤,系統無法檢測,不能作為前置條件;同樣,“收銀”用例的後置條件也是不對的。顧客是否已經帶着貨物離開商店,系統也無法檢測,不能作為後置條件。

2.2.1.2、前置條件必須是用例開始前系統能檢測的




前置條件必須是用例開始前系統能檢測的

 以上圖所示,儲戶開始取現金的交互前,系統不知道儲戶是誰,要去多少錢,所以無法檢測“儲戶賬戶里有足夠的金額”這個條件。如果把前置條件設置為類似於“存在大於最低限額的現金”這樣的背景條件作為前提條件是可以的。就算很長時間沒人來ATM取現金,這個條件是否成立就擺在那裡。

2.2.1.3、前置後置條件是狀態,不是動作

例如,“經理→批假”的前置條件不能寫“員工提交請假單”,因為是一個動作不是狀態,應改為“存在待審批的請假單”。特別要注意的是,寫成“員工已經提交請假單”很可能也是不對的,因為狀態和導致達到某個狀態的行為不是一一對應的,請假單未必是員工自己提交的,也可以組長負責幫本組人員請假,也可能是從另外的系統批量導入。

如果分不清狀態和行為的區別,建模就會遇到很大的麻煩。後面的建模工作中,還會不斷討論狀態和行為的問題。

2.2.1.4、前置後置條件要用核心域詞彙描述

“系統正常運行”、“網絡連接正常”等放之四海而皆準的約束,和所研究系統沒有特定關係,不需要在前置條件中寫出來,否則會得到一堆沒有任何增值作用的廢話。

後置條件也不能簡單地把用例的名字加上“成功”二字變成“XXX成功”。例如“顧客→下單”的後置條件寫成“顧客已經成功下單”,這不是廢話嗎?更合適的後置條件是“訂單信息已保存”。

2.2.1.5、“已登錄”不應作為前置條件

“已登錄”是一個比較有爭議的情況,以購物網站為研究對象,登錄不是用例。這一點已經在前面的已經學習過,那如何處理登錄?

1)畫法一:把其他用例作為“登錄”的擴展




畫法一:把其他用例作為“登錄”的擴展

 

會員登錄后可以下單,也可以查看以往訂單,還可以退貨……所以上圖這個方法把下單、查看以往訂單畫出登錄的擴展。這是錯的。並不是先做A然後做B或C,B和C就成了A的擴展。

2)畫法二:把“登錄”作為被包含用例




畫法二:把“登錄”作為被包含用例

把“登錄”變成被其他用例包含(Include)的被包含用例(Include Use Case)。這樣做是正確的。登錄用例本來不存在,後來在寫用例規約的時候,發現“下單”、“查看以往訂單”等用例都有以下步驟:




“查看以往訂單”步驟

為了節省書寫用例規約的工作量,考慮把這些形成一個小目標的步驟集合(不是單個步驟)分離出來,作為一個被包含用例單獨編寫規約。這個用例只被其他用例包含,不由主執行者指向。所以,如果按照這個做法的話,“下單”用例規約的步驟里,應該有表示包含“登錄”用例的步驟集合:會員【登錄】。這裏的“登錄”二字加了粗括號表示這是一個被包含用例。它的步驟和約束在另外的地方描述。當然,不喜歡用粗括號可以用下劃線等其他方法以示區分。

3)畫法三:其他用例以“已登錄”作為前置條件




畫法三:其他用例以“已登錄”作為前置條件

有些人覺得畫法二會讓好些用例會出現會員【登錄】,看起來有些礙眼,就想能不能把它提到前置條件里,那就得到了畫法三。把“登錄”作為一個用例,“會員已經登錄”作為其他用例的前置條件。這樣用例的步驟看起來更清爽,但是嚴格來說這也是不對的,“登錄”不能作為購物網站的用例。

以上章節學習過,如果在做需求時考慮復用,可能已經陷入了設計的思維。能夠在多個用例中復用登錄的狀態,這是設計人員的本事,他甚至可以做到10個用例的界面都從一個模板生成,但不能因此就把這10個用例合併成一個。

2.2.2、涉眾利益

前提條件是起點,後置條件是終點,中間的路該怎麼走?這就要由涉眾決定了。也就是我們需要對關鍵人按重要程度排序(從前排到後排)去考慮他們的利益,根據這些利益去梳理正確的需求。以銀行ATM為例子,儲戶在ATM取現金的時,涉及的涉眾利益如如下:

● 儲戶:希望方便;擔心權益受損。

● 銀行負責人:希望安全;希望節約運營成本。

正是這些涉眾利益的交鋒之下,目前我們日常生活中所看到的ATM的用例片段如下:




ATM用例片段

從步驟1有設計約束“通過磁條卡或芯片卡提交賬戶號碼”看,這是為了照顧儲戶“方便”的利益。在銀行角度,雖然儲戶是上帝,為了儲戶更加方便,不用密碼更方便的。但從銀行角度要考慮安全問題,不可能不設置密碼,但為什麼只設置6位而不是8位或者更多呢?這又是“安全”和“方便”交鋒后的妥協……

2.2.2.1、涉眾的來源

1)涉眾來源一:人類執行者

用例的執行者如果是人類,當然是用例的涉眾。執行者如果不是人類,就不是涉眾,因為它沒有利益主張。




考慮人類執行者之後的涉眾

上圖保險系統的“內勤→錄入保單”用例中,內勤是人類,是涉眾,而OA系統不是人類,不是涉眾。

2)涉眾來源二:上游

執行者要使用系統做某個用例,可能會需要一些資源,這些資源的提供者可能就是用例的涉眾。還是以“內勤→錄入保單”為例,保單由業務人員代表提供給內勤。如果內勤喝醉了酒亂錄,信息錯得一塌糊塗,業務代表的利益就被損害了。所以,考慮到上游之後,“內勤→錄入保單”用例的涉眾有內勤和業務代表了。




考慮上游之後的涉眾

3)涉眾來源三:下游

執行者使用系統做某個用例,產生的後果會影響到其他人。這些受影響的人也是涉眾。還是以“內勤→錄入保單”為例,如果系統做得不好,沒有檢測內勤錄保單時是否填了必填項就放了過去,後面負責審核的經理工作量增加了。還有,OA系統雖然不是該用例的涉眾,但假如保險系統不停地向OA系統發送垃圾數據包,導致OA癱瘓,那麼OA系統維護人員的工作量就增加了。所以,OA系統維護人員也是下游的涉眾。考慮下游之後,“內勤→錄入保單”用例的涉眾如下圖所示:




考慮下游之後的涉眾

 

4)涉眾來源四:信息的主人

用例會用到一些信息,這些信息會涉及某些人,雖然這些人也許並不知道這個系統的存在,但他們是用例的涉眾。還是以“內勤→錄入保單”為例,保單的信息涉及被保人,投保人和受益人,如果信息出錯或泄露,這些人就會遭殃,所以他們是涉眾。因為這類涉眾可能和系統沒什麼接口,比較容易被忽略,所以要特別需要注意。




考慮信息的主人之後的涉眾

其實,前面的業務建模對識別涉眾起到了非常大的幫助,如果做需求前做了業務建模,會更加了解一件事情的前因後果,大多數涉眾都能夠從業務序列圖中看出來。如下圖所示:




業務建模可以幫助尋找涉眾

 

2.2.2.2、尋找涉眾利益

查理·芒格說過:說服別人要訴諸利益,而非訴諸理性。所以要學會思考涉眾的利益點是一門非常大的學問。尋找涉眾利益時,要“親兄弟,明算賬”,把不同涉眾各自關注的利益體現出來,而不是寫成一模一樣的。家裡兩夫妻對同一件事情都還有不同立場,更不用說一個組織裏面形形色色的涉眾了。例如,司機開車進廠裝化肥,工作人員通過地磅系統操縱地磅給車稱重。針對這件事,不同的涉眾可謂是“各懷鬼胎”:

● 化肥公司老闆——擔心公司內部人員貪污;

● 地磅操作員——希望操作簡便;擔心承擔責任;擔心繫統壞掉影響工作量;

● 倉管人員——擔心稱不準導致無謂的返工裝包;

● 買主——擔心進去時稱得輕了,出來時稱得重了,導致給少了化肥;

● 司機——擔心等候時間太長導致每天拉貨次數減少;

即使有些利益有時不方便白紙黑字寫出來共享,但至少建模人員要心知肚明,不能一團和氣了事。建模人員要仔細觀察和揣摩涉眾的痛苦,才能找到真正的涉眾利益,否則寫出來的“涉眾利益”往往很蒼白。例如以下例子:

● 護士——擔心出錯

這都是正確的無用廢話,誰都擔心出錯,但為什麼還是出錯?仔細調研過之後寫出來就生動多了:

● 護士——擔心自己的藥理學知識記錯,對藥物名稱相近的藥物計算錯劑量,導致給葯錯誤;

2.2.2.3、善於積累涉眾利益

需求是不斷變化的,我想這都是共識了,新系統肯定在功能或性能上和舊系統有所不同,否則還做什麼新系統呢。但是,背後的涉眾利益要穩定得多。看看之前ATM的例子出現的涉眾利益:

儲戶——希望方便;擔心權益受損;

銀行負責人——希望安全;希望節約運營成本;

其實這些涉眾利益不止適用於ATM,也適用於清朝的錢莊櫃檯、現在的銀行櫃檯、網上銀行和手機銀行。換句話說,越本質越穩定。

2.2.3、基本路徑

一個用例會有多個場景,其中有一個場景描述了最成功的情況,執行者和系統的交互非常順利,一路綠燈直抵用例的後置條件。這個場景稱為基本路徑。用例把基本路徑分離出來,目的是凸顯用例的核心價值。還是以ATM為例,發生在ATM上的場景有很多:

1)張三來了,插卡,輸入密碼,輸入金額,順順利利取到錢,高興地走了;

2李四來了,插卡,輸密碼,密碼錯,在輸,再錯,再輸,卡被吞掉了;

3王五來了,插卡,輸密碼,輸金額,今天取得太多不能取了……

以上三個場景,只有場景(1)是銀行在大街上擺放一台ATM的初衷。雖然場景(2)和(3)是難以避免,但場景(1)出現得越多越好,這是涉眾對ATM的期望。

書寫路徑步驟的時候需要注意以下一些要點。這些要點有重疊的地方,如果違反了其中一個要點,很可能會違反另外的要點。

2.2.3.1、按照交互四步曲書寫

執行者和系統按回合交互,直到達成目的。需要的回合數是不定的,可能一個回合足夠,也可能需要多個回合。一個回合中的步驟分為四類:請求、驗證、改變、回應。如下圖所示:




交互四步曲

在一個回合中,請求是必須的,同時還需要其他三類步驟中的至少一類。看看以下例子,可以看到,第一個回合只需要請求和響應,第二個回合則四類步驟都有。




回合制的交互示例

當時間作為主執行者而且不需要和其他輔執行者交互的用例中,可能會出現不需要回應的情況,而且只有一個回合,如下所示:




回合制的交互示例

在書寫步驟時要注意以下一些形式上的問題:

1)對於時間為主執行者的用例,回合中的請求步驟不寫“時間告知時間周期到了”,而是寫“當到達時間周期時”。

2)驗證步驟不寫“是否”。例如以上例子中,第4步寫“系統驗證註冊信息充分”,不寫“系統驗證註冊信息是否充分”,目的是要表達“充分”是基本路徑期望的驗證結果。

3)系統和輔執行者之間的交互可以看作是一種回應步驟,寫成“系統請求輔助執行者做某事”,例如“系統請求郵件列表系統群發郵件”。

2.2.3.2、只寫系統能感知和承諾的內容

看看以下例子:

……

4、系統反饋應收總金額

5、顧客付款

6、收銀員提交付款方式和金額

7、系統計算找零金額

8、系統反饋找零金額,打印收據

9、收銀員找零

……

顧客付款和收銀員找零是系統無法感知和承諾的。如果寫在步驟里,會讓人產生誤解:只要用了本系統,顧客就會乖乖付款,收銀員會乖乖找零——也許顧客忘記付款和收銀員忘記找零正式商場要解決的一個頭痛問題。

2.2.3.3、使用主動語句理清責任

把動作的責任人放在主語的位置,看看以下兩句話:

1)伊布從瓦倫西亞處得到傳球,舒梅切爾撲救伊布的射門。

2瓦倫西亞傳球,伊布射門,舒梅切爾撲救。

雖然上面一句比較文藝,但下面一句把責任理得更清晰。用例步驟也是如此:

系統從會員處獲得用戶名和密碼(錯)

會員提交用戶名和密碼(對)

用戶名和密碼被驗證(錯)

系統驗證用戶名和密碼(對)

會員要是不提交,就不要怪系統沒有動靜;會員要是提交了,系統不動彈,那就要怪系統了。做到規規矩矩說話,把責任理清楚,其實不容易。再列舉一些常見的“胡說八道”,如下所示:




規矩用詞

2.2.3.4、主語只能是主執行者或系統

寫需求,就是要把系統當作一個黑箱,描述它對外提供的功能以及功能附帶的質量需求。系統如何構造,不屬於需求描述的範圍,除非是涉眾強加的設計約束。所以步驟里不能出現“執行者請求前端系統做某事,前端系統請求後端系統做某事”“執行者請求客戶端做某事,客戶端請求服務端做某事”“執行者請求A子系統做某事,A子系統請求B子系統做某事”,就算這個系統最終的組成是分解成很多個部分,分佈在一百多個國家運行,需求里也只有兩個字:系統。前面已經學習過了,系統邊界是責任邊界,而非物理邊界,如下所示:




需求把系統看作是黑箱

 

2.2.3.5、使用核心域術語描述

路徑步驟應該使用核心域的術語來描述,也就是說,要說“人話”。以下以一個零件採購系統為研究對象,比較以下兩句話,哪一句是“人話”,哪一句是“鳥語”?

1)系統建立連接,打開連接,執行SQL語句,從“零件”表查詢……

2系統根據查詢條件搜索零件

其實,一眼就能看出,第一句是“鳥語”,第二句是“人話”了。不同職能多少會有主觀意識,做Java開發的覺得自己做的才是技術,需求屬於業務。但需求人員卻覺得自己做的也是技術,他們所研究的客戶才是業務。但客戶覺得自己做的才是技術……,這咋看起來有點像一個鄙視鏈。其實,大家做的都是“技術”,這是領域不同而已。應該用“核心域”和“非核心域”來代替“業務”和“技術”。

如果所研究系統是一個關係數據庫的腳本工具,核心域是關係數據庫領域,上面提到的“系統建立連接,打開連接,執行SQL語句”就成了合適的需求。

2.2.3.6、不要涉及界面細節

很多人寫需求的時候,會把界面的細節帶進來,例如:

● 會員從下拉框中選擇類別

● 會員從文本框中輸入查詢條件

● 會員單擊“確定”按鈕

這些界面細節很可能不是需求,更多是設計方案,背後可能隱藏更多的真正需求,也許是可用性需求“操作次數不超過5次”。但畢竟我們面對的客戶各種各樣的喜好都有,可能有些前排涉眾明確要求“一定要用下拉框”,那麼“下拉框”也是需求,但依然不能寫“會員從下拉框框中選擇類別”。因為這裏涉及兩類需求,她們的穩定性和變化趨勢不同,應該分開描述:

● 會員選擇類別(這是步驟)

● 通過下拉框來實現(這是設計約束)

用例的需求組織方式是分層的,從用例的路徑、步驟、約束,穩定性會越來越低,如下所示:




用例規約的需求層級

把需求分層級了,穩定和不穩定的需求就分開了,更有利於了解事情的核心本質。平時遇到的大部分“需求變更”發生在補充約束級別,例如輸入會員信息時加個微信字段(字段列表變了),調整結賬時的打折規則(業務規則變了)。級別越高的需求,內容越穩定。

很多時候做需求會把看得見和需求混為一談,真正的需求不見得是顯而易見的。需求判斷的標準不是涉眾是否看得見,而是涉眾是否在意。第一章已經學習過,需求和設計不是一一對應的,而是多對多的。

2.2.3.7、不要涉及交互細節

在步驟中,除了避免描述頁面細節,還要避免描述交互細節。例如有人會這樣寫:

● 會員每輸入賬戶名稱的一個字符

● 系統在界面中驗證當前輸入信息合法

寫的人有他的道理:系統不是等待提交后才驗證輸入信息是否合法,而是隨時驗證立即反饋,這樣使用戶體驗更好。其實,這隻是交互設計的一些技能。忍不住要在需求規約里描述界面和交互的細節,背後的原因和忍不住要思考內部代碼如何實現的原因是一樣的,都是對自己的設計技能沒有信心,害怕“現在想到了如果不忘記記下來以後就忘記了”。這些都是人性的弱點。

用例的步驟應該把焦點放在系統必須接收什麼輸入、系統必須輸出什麼信息以及系統必須做什麼處理這三個重點上,加上字段列表、業務規則、可用性需求等約束,足以表達各種需求。

關於用例的交互怎麼寫,是一個比較頭疼的問題。即使不涉及交互設計細節的問題,也免不了混進交互設計的成分,例如,為什麼要分兩個回合而不是一個回合?實際上涉眾更希望一個回合就能達到用例的目標。所以,建議觀點是在用例規約中把路徑步驟刪掉,只保留輸入輸出、涉眾利益和補充約束,交互的路徑步驟由交互設計人員決定。

2.2.3.8、需求是“不這樣不行”

許多需求人員之所以在需求崗位上,並不是因為他掌握了該掌握的需求技能,可能只是因為他工作年限足夠長該換到需求崗位了——和許多年齡到了就上崗的夫妻和父母相似。這樣的需求人員硬着頭皮做需求時,最常用的一招就是托着腦袋想“這個東西是什麼樣子”,然後畫一個界面原型拿去和涉眾確認。一旦涉眾說“差不多就這樣吧”,就把這個界面原型作為需求交給分析設計人員。在這一點上,互聯網公司的產品經理表現得尤為明顯。如果僥倖成功,就拚命鼓吹“原型大法好”,因為他只會這個。

當需求人員問問題時都是“這樣可以嗎”,相當於:

● 需求人員:界面這樣布局可以嗎?

● 涉    眾:(好用就行,我又不會做界面,問我可不可以我當然說可以了)可以。

● 需求人員:代碼這樣寫可以嗎?

● 涉    眾:(好用就行,我又不會寫代碼,問我可不可以我當然說可以了)可以。

如果問的問題改為“不這樣可以嗎?”,像下面這樣:

● 需求人員:界面不這樣布局可以嗎?

● 涉    眾:不可以,這是政府的規定,你們不要自己亂髮揮啊!

● 需求人員:代碼不這樣寫可以嗎?

● 涉    眾:不可以,這段代碼是我小舅子寫的,一定要這樣,否則不給錢。

這時,界面和代碼就成為了需求,當然,只是補充約束級別的需求。說到這裏,我們歸納出需求的判斷標準:需求是“不這樣不行”,而不是“這樣也行”。

2.2.4、擴展路徑

基本路徑上的每個步驟都有可能發生很多意外,其實某些意外是系統要負責處理的,處理意外的路徑就是擴展路徑。因為一個步驟上出現的意外及其處理可能有多種,所以同一步驟上的擴展路徑可能有多條。




擴展路徑和步驟

對於擴展路徑及其步驟的標號,本書採用的是Cockburn推薦的方法。擴展路徑的標號方法是在擴展步驟的数字序號後面加上字符序號,例如2a表示步驟2的第a條擴展路徑,2b表示步驟2的第b條擴展路徑。擴展路徑條件的最後加上冒號,接下來是該擴展路徑的步驟,標號方法是在擴展路徑編號後面加上数字序號,例如2a1。也就是說,步驟的編號以数字結尾,擴展路徑編號以字母結尾。如果多重擴展,那就繼續按此形式標註。還是以之前ATM“儲戶→取現金”用例為例。該用例規約加上擴展路徑之後如下所示:

基本路徑:

1. 儲戶提交賬戶號碼

2. 系統驗證賬戶號碼合法

3. 系統提示輸入密碼

4. 儲戶輸入密碼

5. 系統驗證密碼合法、正確

6. 系統提示輸入取現金額

7. 儲戶輸入取現金額

8. 系統驗證取現金額合法

9. 系統記錄取現信息,更新賬戶餘額

……

2a. 賬戶號碼不合法:

         2a1.系統反饋賬戶號碼不合法

         2a2.返回1

5a. 密碼不合法:

         5a1.返回3

5b. 密碼合法但不正確:

         5b1.系統驗證當日取款累計輸錯密碼次數不超過3

                   5b1a.當日取款累計輸錯密碼次數超過3次:

                            5b1a1.系統關閉賬戶

                            5b1a2.用例結束

         5b2.系統反饋密碼不正確

         5b3.返回3

8a. 取現金額不合法:

         8a1.返回6

有一點需要注意,和輔執行者交互的步驟很有可能會出現擴展。例如:

4. 系統請求短信平台發布信息

……

擴展

……

4a. 短信平台無響應:

         4a1.系統反饋短信平台無響應

……

但,如果系統不需要從外部系統那裡得到任何結果,這個外系統就不是輔執行者,所以它出現故障會不會導致擴展的討論是沒有意義的。例如:

5. 系統向經理的电子郵箱通知有新的待審批申請

除了以上步驟之外,從其他步驟產生擴展路徑一定要非常謹慎,否則容易讓不屬於需求的內容混進用例規約中。特別要注意下面幾點。

1)能感知和要處理的意外才是擴展

2)設計技能不足導致的錯誤不是擴展

3)不引起交互行為變化的選擇不是擴展

4)界面跳轉不是擴展

2.2.5、補充約束

路徑步驟里描述的需求是不完整的。例如:

用例名:發布講座消息

……

1. 工作人員輸入講座信息,請求發布

2. 系統驗證講座信息充分

3. 系統生成發布內容

4. 系統請求短信平台發布消息

5. 系統保存講座信息和發布情況

6. 系統反饋信息已經保存併發布

……

步驟1中的“講座信息”包括哪些內容?需要添加字段列表。步驟2中“充分”指什麼,需要添加業務規則。從步驟1到步驟6有沒有速度上的要求?需要質量需求。

如果補充約束的內容只和單個用例相關,可以直接放在該用例的規約中;如果補充約束適用於多個用例,可以單獨集中到另外的地方,從用例規約引用。

補充約束前面的編號不代表順序,而是表示該約束綁定的步驟編號,如果某條補充約束不是針對某一步驟,而是針對多個步驟甚至整個用例,前面的編號可以是“*”,如下所示:

5. 發布情況=發布時間+工作人員

補充約束的類型可用類圖表示:




用例的補充約束

2.2.5.1、字段列表

字段列表用來描述步驟里某個領域概念的細節。例如上面“發布講座消息”用例的步驟中,步驟1、3、5都需要分別添加字段列表。

字段列表可以用自然語言表達,例如:

字段列表

1. 講座信息包括:舉辦時間、地點、專家信息、主題、簡介。專家信息包括:姓名、單位、頭銜。

也可以用符號表達,例如:

字段列表

1. 講座信息=舉辦時間+地點+專家+主題+簡介

1. 專家=姓名+單位+頭銜

表示的符號可以採用過去數據字典常用的符號。例如:“+”表示數據序列,“()”表示可選項,“{}”表示多個,“[|||]”表示可能的取值。例如:

註冊信息=公司名+聯繫人+電話+{聯繫地址}

聯繫地=州+城市+街道+(郵編)

保存信息=註冊信息+註冊時間

客房狀態=[空閑|已預定|佔用|維修中]

很多時候,做需求會把設計的細節帶上,例如“電話號碼varchar(255)”或把數據模型圖附上,這是不準確的。數據模型是設計,設計應該來源於需求,而非空想一個設計,然後把他當成需求。

2.2.5.2、業務規則

業務規則描述步驟中系統運算的一些規則,例如上面“發布講座消息”用例的步驟2中的“充分”沒有說清楚,需要添加業務規則,例如:

業務規則:

2. 必須有的項包括:時間、地點、專家、主題

其實,主要涉眾能理解,行業上適用的任何方式(例如數學、物理公式)都可以用來表達業務規則。描述業務規則時要注意:業務規則不等於實現算法。因為業務規則也是一種需求,也是從涉眾的角度看“不這樣不行”的。例如,研究一款為盲人或殘疾人而做的語音輸入軟件,用例規約有如下片段:

3. 系統將語音輸入翻譯為文字

業務規則

3. 採用XX識別算法

這樣的業務規則可能是有問題的,如果前排是盲人或殘疾人,他們是不懂什麼叫“XX算法”,也不在意你是否用了這個算法,他們在意的需求是:“背景噪音強度為XX的情況下,識別率應該在XX以上”。當然也不排除前排涉眾是有意推廣某廠家的識別技術,那麼“採用XX識別算法”也可以成為需求。

2.2.5.3、質量需求

系統滿足功能需求,說明系統能把事情做正確。在做正確的基礎上,系統還需要在做的過程中滿足一些指標,這些指標就是質量需求,也被稱為“非功能需求”。

1)可用性

可用性需求是對人類執行者和系統之間交互質量的度量。如果系統僅能正確達到用例目標,但交互太頻繁,人類執行者是不喜歡用的。在表達可用性需求時,僅僅說“系統容易使用”是不行的。這是無法量化的目標,合適的可用性需求應該是可以度量的,例如:




可用性度量

2)性能

性能包括速度、容量、能力等,例如:

① 系統應在0.5秒之內拍攝超速車的照片(速度)

② 應允許1000個執行者同時使用此用例(容量)

③ 在標準工作負荷下,系統的CPU佔用率應少於50%(能力)

在尋找質量需求時,性能類型的質量需求往往是最多的。

3)可靠性

可靠性表示系統的安全性和完整性,通常用平均無故障時間(MTBF,Mean Time Between Failures)和平均修復時間(MTTR, Mean Time To Repair)表示。可靠性需求往往不是針對單個用例,而是針對整個系統,可以在所有用例規約的最後,單獨用小篇幅描述。

4)可支持性

可支持性表示系統升級和修復的能力。例如:

① 95%的緊急錯誤應能在30個工作時內修復

② 在修復故障時,未修復的相關缺陷平均數應小於0.5

③ 升級新版本時,應保存所有系統設置和個人設置

和可靠性一樣,可支持性需求往往不是針對單個用例,而是針對整個系統,可以在所有用例規約的最後,單獨用小篇幅描述。

以上介紹了質量需求的種類,很多時候質量需求不用可以去尋找,按照前面說過的“不這樣行嗎”的標準,把混入需求的設計刪掉,然後問為什麼,背後往往隱含的就是質量需求,如下所示:




質量需求

2.2.5.4、設計約束

設計約束是在實現系統時必須要遵守的一些約束,包括界面樣式、報表格式、平台、語言等。

設計約束既不是功能需求,也不是質量需求。例如,“用Oracle數據庫保存數據”,其實用DB2也可以滿足同樣的功能需求和質量需求,但必須要用Oracle,因為客戶已經採購了許多Oracle數據庫軟件,如果不用,成本就會增加。

設計約束是需求的一種,也一樣要從涉眾的視角描述。在很多需求規約中,不少來自開發人員視角的設計偽裝成設計約束,混進了需求的隊伍。例如“系統應採用三層架構方式搭建”,涉眾並不了解“三層”好在哪裡,為什麼不是四層、五層等,然後問“為什麼”,背後的真正需求可能還是性能需求。

需求是問題,設計是解決方案,二者的穩定性不同。

 

2.3、需求啟發

2.3.1、需求啟發要點

需求人員要能夠像獵人一樣,用銳利的眼睛發現隱藏在叢林中的獵物,像偵探一樣,用縝密的思維判斷出偽裝成好人的兇手。但需求人員會有許多的障礙:如

1)需求的一個啟發障礙是知識的詛咒,意思是:一旦知道某個東西,就很難相像不知道它會是什麼樣子;

2)知識的詛咒在需求啟發中體現為溝通的困難;

3)需求啟發的另一個障礙是做和定義的不同;

但理解了以下兩個要點,有助於克服需求啟發中的障礙:

1)和涉眾交流的形式應該採用視圖,而不是模型;

2)和涉眾交流的內容應該聚焦涉眾利益,而不是需求;

2.3.2、需求啟發手段




需求啟發手段

2.3.3、需求人員的素質培養




需求人員的素質

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

(三十五)golang–面向對象之多態

多態:變量具有多種形態,可以用統一的接口來調用不同的實現。

接口體現多態特徵:

(1)多態參數:之前所講的Usb接口案例,既可以接受手機變量,也可以接受相機變量,就體現了usb接口的多態;

(2)多態數組:

package main

import (
    "fmt"
)

type usb interface {
    start()
    stop()
}

type phone struct {
    name string
}

func (p phone) start() {
    fmt.Println(p.name, "手機開始工作")
}

func (p phone) stop() {
    fmt.Println(p.name, "手機停止工作")
}

type camera struct {
    name string
}

func (c camera) start() {
    fmt.Println(c.name, "相機開始工作")
}

func (c camera) stop() {
    fmt.Println(c.name, "相機停止工作")
}

type computer struct {
}

func (co computer) working(usb usb) {
    usb.start()
    usb.stop()
}

func main() {
    var usbArr [3]usb
    usbArr[0] = phone{"小米"}
    usbArr[1] = phone{"vivo"}
    usbArr[2] = camera{"尼康"}
    fmt.Println(usbArr)
    for i := 0; i < len(usbArr); i++ {
        usbArr[i].start()
        usbArr[i].stop()
    }
}

我們以前講到,數組是只能存儲同一種類型的數據,利用多態數組,就可以存儲不同的類型了;

如何將一個接口變量賦值給一個自定義類型的變量?使用類型斷言

類型斷言:由於接口是一般類型,不知道具體類型,如果要轉成具體類型,就需要使用類型斷言;要保持原來空接口指向的數據類型和斷言的數據類型一致;

為了避免輸出panic報錯,可以進行斷言判斷;

類型斷言實踐一:

我們給phone中加入一個方法call(),在調用usb變量時,usb.call(),肯定是不對的,因為usb可能是phone,也可能是camera,而camera是沒有這個函數的,因此,在調用的時候用類型斷言。

package main

import (
    "fmt"
)

type usb interface {
    start()
    stop()
}

type phone struct {
    name string
}

func (p phone) start() {
    fmt.Println(p.name, "手機開始工作")
}

func (p phone) call() {
    fmt.Println(p.name,"手機在打電話")
}

func (p phone) stop() {
    fmt.Println(p.name, "手機停止工作")
}

type camera struct {
    name string
}

func (c camera) start() {
    fmt.Println(c.name, "相機開始工作")
}

func (c camera) stop() {
    fmt.Println(c.name, "相機停止工作")
}

type computer struct {
}

func (co computer) working(usb usb) {
    usb.start()
    //如果usb還指向phone的結構體變量,則還需要調用call方法
    if phone, ok := usb.(phone); ok { phone.call() }
    usb.stop()
}

func main() {
    var usbArr [3]usb
    usbArr[0] = phone{"小米"}
    usbArr[1] = phone{"vivo"}
    usbArr[2] = camera{"尼康"}
    var com computer
    fmt.Println(usbArr)
    for i := 0; i < len(usbArr); i++ {
        com.working(usbArr[i])
    }
}

類型斷言實踐2:循環判斷輸入參數的類型

package main

import (
    "fmt"
)

type student struct {
    name string
}

func typeJudge(items ...interface{}) {
    for index, x := range items {
        switch x.(type) {
        case bool:
            fmt.Printf("第%v個參數是bool類型,值是%v\n", index, x)
        case int, int32, int64:
            fmt.Printf("第%v個參數是整數類型,值是%v\n", index, x)
        case float32:
            fmt.Printf("第%v個參數是float32類型,值是%v\n", index, x)
        case float64:
            fmt.Printf("第%v個參數是float64類型,值是%v\n", index, x)
        case string:
            fmt.Printf("第%v個參數是string類型,值是%v\n", index, x)
        case student:
            fmt.Printf("第%v個參數是student類型,值是%v\n", index, x)
        case *student:
            fmt.Printf("第%v個參數是*student類型,值是%v\n", index, x)
        default:
            fmt.Printf("第%v個參數類型不確定,值是%v\n", index, x)
        }
    }
}

func main() {
    var n1 float32 = 1.1
    var n2 float64 = 1.2
    var n3 int32 = 1
    var name string = "tom"
    var n5 bool = true

    stu1 := student{"jack"}
    stu2 := &student{"bob"}

    typeJudge(n1, n2, n3, name, n5, stu1, stu2)
}

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

I/O多路復用模型

背景

在文章中提到了五種I/O模型,其中前四種:阻塞模型、非阻塞模型、信號驅動模型、I/O復用模型都是同步模型;還有一種是異步模型。

想寫一個系列的文章,介紹從I/O多路復用到異步編程和RPC框架,整個演進過程,這一系列可能包括:

  1. Reactor和Proactor模型
  2. 為什麼需要異步編程
  3. enable_shared_from_this用法分析
  4. 網絡通信庫和RPC

為什麼有多路復用?

多路復用技術要解決的是“通信”問題,解決核心在於“同步事件分離器”(de-multiplexer),linux系統帶有的分離器select、poll、epoll網上介紹的比較多,大家可以看看這篇介紹的不錯的文章:。通信的一方想要知道另一方的狀態(以決定自己做什麼),有兩種方法: 一是輪詢,二是消息通知。

輪詢

輪詢的一種典型的實現可能是這樣的:當然這裏的epoll_wait()也可以使用poll()或者select()替換。

whiletrue) {
    active_stream[] = epoll_wait(epollfd)
    for i in active_stream[] {
        read or write till
    }
}

輪詢方式主要存在以下不足:

  • 增加系統開銷。無論是任務輪詢還是定時器輪詢都需要消耗對應的系統資源。
  • 無法及時感知設備狀態變化。在輪詢間隔內的設備狀態變化只有在下次輪詢時才能被發現,這將無法滿足對實時性敏感的應用場合。
  • 浪費CPU資源。無論設備是否發生狀態改變,輪詢總在進行。在實際情況中,大多數設備的狀態改變通常不會那麼頻繁,輪詢空轉將白白浪費CPU時間片。

消息通知

其實現方式通常是: “阻塞-通知”機制。阻塞會導致一個任務(task_struct,進程或者線程)只能處理一個”I/O流”或者類似的操作,要處理多個,就要多個任務(需要多個進程或線程),因此靈活性上又不如輪詢(一個任務足夠),很矛盾。

 

select、poll、epoll對比

矛盾的根源就是”一”和”多”的矛盾: 希望一個任務處理多個對象,同時避免處理阻塞-通知機制的內部細節。解決方案是多路復用(muliplex)。多路復用有3種基本方案,select()/poll()/epoll(),都是來解決這一矛盾的。

  • 通知代理: 用戶把需要關心的對象註冊給select()/poll()/epoll()函數。
  • 一對多: 所有的被關心的對象,只要有一個對象有了通知事件,select()/poll()/epoll()就會結束阻塞狀態。
  • 方便性: 用戶(程序員)不用再關心如何阻塞和被通知,以及哪些情況下會有通知產生。這件事情已經由上述幾個系統調用做了,用戶只需要實現”通知來了我該做什麼”。

 

那麼上面3個系統調用的區別是什麼呢?
第一個select(),結合了輪詢和阻塞兩種方式,沒有問題,每次有一個對象事件發生的時候,select()只是知道有事件發生了,具體是哪個對象發生的,不知道,需要從頭到尾輪詢一遍,複雜度是O(n)。poll函數相對select函數變化不大,只是提升了最大的可輪詢的對象個數。epoll函數把時間複雜度降到O(1)。

 

為什麼select慢而epoll效率高?
select()之所以慢,有幾個原因: select()的參數是一個FD數組,意味着每次select調用,都是一次新的註冊-阻塞-回調,每次select都要把一個數組從用戶空間拷貝到內核空間,內核檢測到某個對象狀態變化並寫入后,再從內核空間拷貝回用戶空間,select再把這個數組讀取一遍,並返回。這個過程非常低效。

epoll的解決方案相當於是一種對select()的算法優化: 它把select()一個函數做的事情分解成了3步,首先epoll_create()創建一個epollfd對象(相當於一個池子),然後所有被監聽的fd通過epoll_ctrl()註冊到這個池子,也就是為每個fd指定了一個內部的回調函數(這樣,就沒有了每次調用時的來回拷貝,用戶空間的數組到內核空間只有這一次拷貝)。epoll_wait阻塞等待。在內核態有一個和epoll_wait對應的函數調用,把就緒的fd,填入到一個就緒列表中,而epoll_wait讀取這個就緒列表,做到了快速返回(O(1))。

詳細的對比可以參考select、poll、epoll之間的區別總結:

 

有了上面的原理介紹,這裏舉例來說明下epoll到底是怎麼使用的,加深理解。舉兩個例子:

一個是比較簡單的父子進程通信的例子,單個小程序,不需要跑多個應用實例,不需要用戶輸入。
一個是比較實戰的socket+epoll,畢竟現實案例中哪有兩個父子進程間通訊這麼簡單的應用場景。

有了多路復用,難道還不夠?

有了I/O復用,有了epoll已經可以使服務器併發幾十萬連接的同時,維持高TPS了,難道這還不夠嗎?答案是,技術層面足夠了,但在軟件工程層面卻是不夠的。例如,總要有個for循環去調用epoll,總來處理epoll的返回,這是每次都要重複的工作。for循環體裏面寫什麼—-通知返回之後,做事情的程序最好能以一種回調的機制,提供一個編程框架,讓程序更有結構一些。另一方面,如果希望每個事件通知之後,做的事情能有機會被代理到某個線程裏面去單獨運行,而線程完成的狀態又能通知回主任務,那麼”異步”的進制就必須被引入。

所以,還有兩個問題要解決,一是”編程框架”,一是”異步”。我們先看幾個目前流行的框架,大部分框架已經包含了某種異步的機制。我們接下來的篇章將介紹“編程框架”和“異步I/O模型”。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

MySQL 5.7 – 通過 BINLOG 恢複數據

日常開發,運維中,經常會出現誤刪數據的情況。誤刪數據的類型大致可分為以下幾類:

  • 使用 delete 誤刪行
  • 使用 drop table 或 truncate table 誤刪表
  • 使用 drop database 語句誤刪數據庫
  • 使用 rm 命令誤刪整個 MySQL 實例。

不同的情況,都會有其優先的解決方案:

  • 針對誤刪行,可以通過 Flashback 工具將數據恢復
  • 針對誤刪表或庫,一般採用通過 BINLOG 將數據恢復。
  • 而對於誤刪 MySQL 實例,則需要我們搭建 HA 的 MySQL 集群,並保證我們的數據跨機房,跨城市保存。

本篇主要討論的內容是誤刪表或者庫,會先介紹有關 BINLOG 的操作命令,然後會對誤刪表的這種情況進行實際的模擬。

BINLOG 常見操作命令

BINLOG 的查詢方式一般分為兩種,一種是進入 MySQL 控制台進行查詢,另一種是通過 MySQL 提供的工具 mysqlbinlog 進行查詢,兩者的不同會在下面介紹。

通過 MySQL Cli 查詢 BINLOG 信息

在 cli 中,常見的命令如下:

# 查詢 BINLOG 格式
show VARIABLES like 'binlog_format';

# 查詢 BINLOG 位置
show VARIABLES like 'datadir';

# 查詢當前數據庫中 BINLOG 名稱及大小
show binary logs;

# 查看 master 正在寫入的 BINLOG 信息
show master status\G;

# 通過 offset 查看 BINLOG 信息
show BINLOG events in 'mysql-bin.000034' limit 9000,  10;

# 通過 position 查看 binlog 信息
show BINLOG events in 'mysql-bin.000034' from 1742635 limit 10;

使用 show BINLOG events 的問題:

  • 使用該命令時,如果當前 binlog 文件很大,而且沒有指定 limit,會引發對資源的過度消耗。因為 MySQL 客戶端需要將 binlog 的全部內容處理,返回並显示出來。為了防止這種情況,mysqlbinlog 工具是一個很好的選擇。

通過 mysqlbinlog 查詢 BINLOG 信息

在介紹 mysqlbinlog 工具使用前,先來看下 BINLOG 文件的內容:

# 查詢 BINLOG 的信息
mysqlbinlog  --no-defaults mysql-bin.000034 | less
# at 141
#100309  9:28:36 server id 123  end_log_pos 245
  Query thread_id=3350  exec_time=11  error_code=0
  • at 表示 offset 或者說事件開始的起始位置
  • 100309 9:28:36 server id 123 表示 server 123 開始執行事件的日期
  • end_log_pos 245 表示事件的結束位置 + 1,或者說是下一個事件的起始位置。
  • exec_time 表示在 master 上花費的時間,在 salve 上,記錄的時間是從 Master 記錄開始,一直到 Slave 結束完成所花費的時間。
  • rror_code=0 表示沒有錯誤發生。

在大致了解 binlog 的內容后,mysqlbinlog 的用途有哪些?:

  • mysqlbinlog 可以作為代替 cli 讀取 binlog 的工具。
  • mysqlbinlog 可以將執行過的 SQL 語句輸出,用於數據的恢復或備份。

查詢 BINLOG 日誌:

# 查詢規定時候后發生的 BINLOG 日誌
mysqlbinlog --no-defaults --base64-output=decode-rows -v  --start-datetime  "2019-11-22 14:00:00" --database sync_test  mysql-bin.000034 | less

導出 BINLOG 日誌,用於分析和排查 sql 語句:

mysqlbinlog --no-defaults --base64-output=decode-rows -v  --start-datetime  "2019-11-22 14:00:00" --database sync_test  mysql-bin.000034 > /home/mysql_backup/binlog_raw.sql

導入 BINLOG 日誌

# 通過 BINLOG 進行恢復。
mysqlbinlog --start-position=1038 --stop-position=1164 --database=db_name  mysql-bin.000034 | mysql  -u cisco -p db_name

# 通過 BINLOG 導出的 sql 進行恢復。
mysql -u cisco -p db_name < binlog_raw.sql.sql

mysqlbinlog 的常用參數:

  • --database 僅僅列出配置的數據庫信息
  • --no-defaults 讀取沒有選項的文件, 指定的原因是由於 mysqlbinlog 無法識別 BINLOG 中的 default-character-set=utf8 指令
  • --offset 跳過 log 中 N 個條目
  • --verbose 將日誌信息重建為原始的 SQL 陳述。
    • -v 僅僅解釋行信息
    • -vv 不但解釋行信息,還將 SQL 列類型的註釋信息也解析出來
  • --start-datetime 显示從指定的時間或之後的時間的事件。
    • 接收 DATETIME 或者 TIMESTRAMP 格式。
  • --base64-output=decode-rows 將 BINLOG 語句中事件以 base-64 的編碼显示,對一些二進制的內容進行屏蔽。
    • AUTO 默認參數,自動显示 BINLOG 中的必要的語句
    • NEVER 不會显示任何的 BINLOG 語句,如果遇到必須显示的 BINLOG 語言,則會報錯退出。
    • DECODE-ROWS 显示通過 -v 显示出來的 SQL 信息,過濾到一些 BINLOG 二進制數據。

MySQL Cli 和 mysqlbinlog 工具之間的比較

如果想知道當前 MySQL 中正在寫入的 BINLOG 的名稱,大小等基本信息時,可以通過 Cli 相關的命令來查詢。

但想查詢,定位,恢復 BINLOG 中具體的數據時,要通過 mysqlbinlog 工具,因為相較於 Cli 來說,mysqlbinlog 提供了 --start-datetime--stop-position 等這樣更為豐富的參數供我們選擇。這時 Cli 中 SHOW BINLOG EVENTS 的簡要語法就變得相形見絀了。

使用 BINLOG 恢複數據

恢復的大致流程如下:

  1. 會創建數據庫和表,並插入數據。
  2. 誤刪一條數據。
  3. 繼續插入數據。
  4. 誤刪表。
  5. 最後將原來以及之後插入的數據進行恢復。

準備數據

準備數據庫,表及數據:

# 創建臨時數據庫
CREATE DATABASE IF NOT EXISTS test_binlog default charset utf8 COLLATE utf8_general_ci; 


# 創建臨時表
CREATE TABLE `sync_test` (`id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;


# 添加數據
insert into sync_test (id, name) values (null, 'xiaoa');
insert into sync_test (id, name) values (null, 'xiaob');
insert into sync_test (id, name) values (null, 'xiaoc');

# 查看添加的數據
select * from sync_test;

刪除表或者數據

誤刪操作:

# 刪除 name=xiaob 的數據
delete from sync_test where id=3

# 插入幾條數據
insert into sync_test (id, name) values (null, 'xiaod');
insert into sync_test (id, name) values (null, 'xiaoe');
insert into sync_test (id, name) values (null, 'xiaof');

# 刪除表
DROP TABLE sync_test;

數據的恢復

在執行數據恢復前,如果操作的是生產環境,會有如下的建議:

  • 使用 flush logs 命令,替換當前主庫中正在使用的 binlog 文件,好處如下:
    • 可將誤刪操作,定位在一個 BINLOG 文件中,便於之后的數據分析和恢復。
    • 避免操作正在被使用的 BINLOG 文件,防止發生意外情況。
  • 數據的恢復不要在生產庫中執行,先在臨時庫恢復,確認無誤后,再倒回生產庫。防止對數據的二次傷害。

通常來說,恢復主要有兩個步驟:

  1. 在臨時庫中,恢復定期執行的全量備份數據。
  2. 然後基於全量備份的數據點,通過 BINLOG 來恢復誤操作和正常的數據。

使用 BINLOG 做數據恢復前:

# 查看正在使用的 Binlog 文件
show master status\G;
# 显示結果是: mysql-bin.000034

# 執行 flush logs 操作,生成新的 BINLOG
flush logs;

# 查看正在使用的 Binlog 文件
show master status\G;
# 結果是:mysql-bin.000035

確定恢複數據的步驟:

這裏主要是有兩條誤刪的操作,數據行的誤刪和表的誤刪。有兩種方式進行恢復。

  • 方式一:首先恢復到刪除表操作之前的位置,然後再單獨恢復誤刪的數據行。
  • 方式二:首先恢復到誤刪數據行的之前的位置,然後跳過誤刪事件再恢複數據表操作之前的位置。

這裏採用方式一的方案進行演示,由於是演示,就不額外找一個臨時庫進行全量恢復了,直接進行操作。

查詢創建表的事件位置和刪除表的事件位置

#  根據時間確定位置信息
mysqlbinlog --no-defaults --base64-output=decode-rows -v  --start-datetime  "2019-11-22 14:00:00" --database test_binlog  mysql-bin.000034 | less

創建表的開始位置:

刪除表的結束位置:

插入 name=’xiaob’ 的位置:

# 根據位置導出 SQL 文件
mysqlbinlog --no-defaults --base64-output=decode-rows -v --start-position "2508132" --stop-position "2511004" --database test_binlog  mysql-bin.000034 > /home/mysql_backup/test_binlog_step1.sql
 
 
mysqlbinlog --no-defaults --base64-output=decode-rows -v --start-position "2508813" --stop-position "2509187" --database test_binlog  mysql-bin.000034 > /home/mysql_backup/test_binlog_step2.sql
 

# 使用 mysql 進行恢復
mysql -u cisco -p < /home/mysql_backup/test_binlog_step1.sql
mysql -u cisco -p < /home/mysql_backup/test_binlog_step2.sql

MySQL 5.7 中無論是否打開 GTID 的配置,在每次事務開啟時,都首先會出 GTID 的一個事務,用於并行複製。所以在確定導出開始事務位置時,要算上這個事件。

在使用 –stop-position 導出時,會導出在指定位置的前一個事件,所以這裏要推后一個事務。

對於 DML 的語句,主要結束位置要算上 COMMIT 的位置。

總結

在文章開始時,我們熟悉了操作 BINLOG 的兩種方式 CLI 和 mysqlbinlog 工具,接着介紹了其間的區別和使用場景,對於一些大型的 BINLOG 文件,使用 mysqlbinlog 會更加的方便和效率。並對 mysqlbinlog 的一些常見參數進行了介紹。

接着通過使用 mysqlbinlog 實際模擬了數據恢復的過程,並在恢複數據時,提出了一些需要注意的事項,比如 flush logs 等。

最後在恢複數據時,要注意 start-positionend-position 的一些小細節,來保證找到合適的位置。

參考

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!