Quantcast
Channel: 黑暗執行緒
Viewing all 2304 articles
Browse latest View live

徹底移除已簽入TFS的項目

0
0

保留完整版本變更歷程是版控系統的核心精神之一,檔案項目一旦簽入,就算使用者要求刪除,項目從清單上消失,仍可透過歷史記錄還原每一個曾簽入的版本。

實務上,偶爾會發生不慎誤將不該簽入內容丟上版控的狀況(例如:誤簽入個資或機密敏感內容),此時版控對保留完整軌跡的堅持變成缺點,不管刪除或 Rollback 都無法防止他人透過歷史記錄還原內容。

非常狀況只能用非常手段,在 TFS 上遇此種狀況,tf.exe 工具有個 destroy 指令可以解決問題。

語法範例如下:

tf destroy $/src/path/filename.ext /collection:"httq://tfs-server:8080/tfs/collectionName"

執行者必須具備 Team Foundation Administrators 管理者群組身分,確認刪除後檔案便會從 TFS 移除,從 Changeset、Pending Change、Merge History、Branch History 徹底消失,不留半點痕跡。參考

在 destroy 前,建議先依 TFS 標準做法將該項目刪除。否則可能出現類似下圖的奇特狀態,tfs-destroy-test.txt 已從 Source Controller Explorer 消失,但 Solution Explorer 裡它仍存在且有藍色鎖頭,按右鍵 View History 也能查詢,但查不到任何簽入簽出記錄。

若 destroy 時其他人更動過該檔案而處於 Pending Change(暫止變更)狀況,將出現如下警示:

此時可加上 /preview 參數,tf.exe 將不執行動作,只顯示檔案被哪些人列為 Pending Change。優雅的做法是通知相關人員自行 Undo 後再 destroy;粗暴一點直接刪除也成,該項目將從眾人的 Pending Change 清單消失,可能讓當事人一頭霧水以為撞鬼。

最後提醒,destroy 非正常操作,只能視為修補錯誤簽入動作的迫不得以手段,並有遺失修改歷程、破壞資料一致性的副作用,只能由管理者下執行,但遇到不計代價必須抹除資料的情境,算是唯一的解法。


小試 IIS 的簡易 DoS 防護-動態 IP 限制

0
0

這幾天,DDoS 攻擊事件在台灣鬧得沸沸揚揚。

DoS 攻擊可約略分為頻寬消耗型(找一大群鄉民擠在餐廳門口)及資源消耗型(召喚服務生過來點菜連點兩鐘頭,或一口氣點兩百盤紅燒獅子頭),從網站管理者的角度,對頻寬消耗型攻擊完全無能為力,只能靠 ISP 或網管單位防禦;但對於資源消耗型攻擊,倒是有些因應對策。(例如:把十分鐘還沒點完菜的客人攆出去或把點兩百盤獅子頭的客人打成豬頭)

我知道有一些 ASP.NET 設定就與 DoS 防護有關,例如:每次搞檔案上傳時都要手動調大的 MaxRequestLength參數,選用偏小的 4MB 預設值,用意就在防止惡意程式透過 POST Request 傳送大量資料耗盡頻寬、記憶體或執行緒。而這陣子詢問度很高則是另一項 IIS功能-「動態 IP 限制」(在 IIS 7 時代為額外模組需透過 Web PI 安裝,IIS 8 起改為內建,詳情可參考小朱的文章), 過去曾聽過有此功能,但沒實際玩過,把握這個機會弄個小 Lab 觀察驗證也好。

以 IIS 8 為例,先從 IIS 管理員開啟「IP位址及網域限制」:

點選「編輯動態限制設定…」即可叫出設定畫面:

動態 IP 限制可從兩個方向下手:限制同一 IP 同時開啟連線數或限制同一 IP 在一段期間內發送的最大要求數量, 一旦超過上限,IIS 便直接回傳錯誤,不再耗費頻寬或 CPU、記憶體處理要求。

預設建議值為每個 IP 同時連線數不超過 5 條,每 0.2 秒要求數不得超過 20 個,超過這個數字 IIS 就暫時不處理來自該 IP 的請求,直接傳回 HTTP 錯誤以節省資源。

為印證效果,我設計了以下兩個實驗:

【實驗1】

用 IFrame 同時開啟超過五個網頁,網頁故意超過 1 秒傳回結果,迫使瀏覽器同時開啟多條連線,以突破同時要求數上限。

TestSimuLimit.html (開啟七個 IFrame 載入 SlowTask.aspx)

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8"/>
<style>
        iframe { 
            display: block; margin: 3px; width: 600px; height: 50px;
        }
</style>
</head>
<body>
<script>
for (var i = 0; i < 7; i++) {
            document.write('<iframe src="SlowTask.aspx?t=' + Math.random() + '"></iframe>');
        }
</script>
</body>
</html>

SlowTask.aspx (延遲 1 秒傳回 Guid)

<%@ Page Language="C#" %>
<%
    System.Threading.Thread.Sleep(1000);
    Response.ContentType = "text/plain";
    Response.Write(Guid.NewGuid());
    Response.End();
%>

測試結果:前五個 IFrame 顯示正常,後兩個傳回 403.501。註:
501: Dynamic IP Restrictions rejected the request due to too many concurrent requests
502: Dynamic IP Restrictions rejected the request due to too many requests over time

【實驗2】

以 setInterval 方式連續以 $.get() 方式發出 30 個 GET 存取,藉由改變時間間隔調整單位時間內發出要求數,例如:間隔 10ms 等於每 200ms 發出 20 個 AJAX 要求。

TestRPSLimit.html

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8"/>
<style>
        li {
            display: inline-block; margin: 3px; padding: 3px;
            border: 1px solid gray;
        }
        .warn {
            background-color: sandybrown;
        }
</style>
</head>
<body>
    Interval = <inputtype="text"value="11"size="2"/> ms
<buttonid="btnRun">Test</button>
<ulid="ulResults"></ul>
<scriptsrc="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script>
var MAX_COUNT = 30;
var ul = $("ul");
var count = 0;
function test() {
            ul.empty();
            count = MAX_COUNT;
var hnd = setInterval(function () {
                $.get("OK.txt?t=" + Math.random()).done(function () {
                    ul.append("<li>Succ</li>");
                }).fail(function () {
                    ul.append("<li class='warn'>Fail</li>");
                });
                count--;
if (count <= 0) clearInterval(hnd);
            }, parseInt($("input").val()));
        }
        $("button").click(test);
</script>
</body>
</html>

間隔 11ms,相當於 18.2 Requests / 200ms,未突破上限,所有 $.get() 都成功。

間隔 9ms,約 22.2 Requests / 200 ms,我們觀察到前 20 次成功,接著有三次失敗(由 F12 開發者工具可知為 HTTP 403),滿 200ms 後重新計算,後面 7 次也成功。

間隔 7ms,約 28.6 Requests / 200 ms,前 20 次成功,接著 8 次失敗,滿 200ms 重新計算,後面又成功。

【結論】

透過上述兩個實驗,我們驗證 IIS 的動態 IP 限制確實能降低單一 IP 同時或連續發出大量請求拖垮系統的風險,雖然功能與強韌度無法與專業 DDoS 防禦設備相提並論,但仍具有基本的防護效力(甚至對付「網頁搶票 F5 Combo攻擊」也多少有效果,哈),值得一試。

實務設定時,同時要求數及連續要求數的拿捏是門學問,較嚴謹的做法是觀察平日負載下單一 IP 可能產生的要求數,以判定超過多少需視為異常。設定上限數字的畫面有個「僅啟用記錄模式」選項,此時就能派上用場,在記錄模式下,IIS 遇到超過上限時仍會以 HTTP 200 正常傳回結果,但 IIS Log 的 sc-substatus 會註記 501/502 做為區別,以便觀察誤判比率。有個比較麻煩的問題,是以來源 IP 做為判定依據,來自同一 NAT、防火牆、代理伺服器後方的使用者會共用同一 IP,有可能因誤判造成困擾,此點在是高流量網站更明顯。針對大型網站或重要服務,恐怕還是得靠專業 DoS 防護設備較有保障。

使用非同步處理提升資料庫更新速度

0
0

來自同事的資料庫程式效能調校案例一則。

情境為一支同步來源及目的資料表的排程,先一次取回來源及目的資料表,逐一檢查資料是否已存在目的資料表,若不存在即執行Insert,若存在則執行 Update 更新欄位。因 Insert/Update 之前需進行特定轉換,故難以改寫為 Stored Procedure。排程有執行過慢問題,處理四萬筆資料耗時近 27 分鐘。

程式示意如下:

foreach (var src in srcList)
{
try
    {
        var target = findExistingData(src);
if (target == null)
        {
            AddTargetToDB(src);
        }
else
        {
            UpdateTargetToDB(target, src);
        }
    }
catch (Exception e)
    {
        LogError(e);
    }
}

同事加入多執行緒平行處理,改寫為 Parallel.ForEach 版本如下,很神奇地把時間縮短到 5 分鐘內完成!

var count = 0;
Parallel.ForEach(srcList, () => 0, (src, state, subtotal) =>
{
try
    {
        var target = FindExistingData(src);
if (target == null)
        {
return AddTargetToDB(src);
        }
else
        {
return UpdateTargetToDB(target, src);
        }
    }
catch (Exception e)
    {
        LogError(e);
return 0;
    }
},
rowsEffected =>
{
    Interlocked.Add(ref count, rowsEffected);
});

加入平行處理可加速在預期之內,高達五倍的效能提升卻讓我大吃一驚!我原本預期,四萬次 Insert 或 Update 操作大批進入應該在資料庫端也會形成瓶頸,例如:若 Insert 或 Update 涉及 Unique Index,資料庫端需依賴鎖定機制防止資料重複,即使同時送入多個執行指令,進了資料庫還是得排隊執行。

仔細分析,此案例靠多核平行運算能產生的效益有限,效能提升主要來自節省網路傳輪的等待時間。為此,我設計了一個實驗:建主一個包含 12 個欄位的資料表,4 個 VARCHAR(16)、4 個 INT、4 個 DATETIME,使用以下程式測試用 foreach 及 Parallel.ForEach 分別執行 1024, 2048, 4096, 8192 筆資料的新增與更新並記錄時間,Parallel.ForEach 部分則加入同時執行的最大執行緒數目統計:

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dapper;
using System.Diagnostics;
using System.Threading;
 
namespace BatchInsert
{
class Program
    {
staticstring cs = "連線字串";
staticstring truncCommand = @"TRUNCATE TABLE Sample";
staticstring insertCommand = @"
INSERT INTO Sample (T1,T2,T3,T4,N1,N2,N3,N4,D1,D2,D3,D4) 
VALUES (@T1,@T2,@T3,@T4,@N1,@N2,@N3,@N4,@D1,@D2,@D3,@D4)";
staticstring updateCommand = @"
UPDATE [dbo].[Sample]
   SET [T2] = @T2, [T3] = @T3, [T4] = @T4
      ,[N1] = @N1, [N2] = @N2, [N3] = @N3, [N4] = @N4
      ,[D1] = @D1, [D2] = @D2, [D3] = @D3, [D4] = @D4
 WHERE T1 = @T1";
staticvoid Main(string[] args)
        {
            Test(1024);
            Test(2048);
            Test(4096);
            Test(8192);
            Console.Read();
        }
 
staticvoid Test(int count)
        {
            List<DynamicParameters> data = new List<DynamicParameters>();
for (var i = 0; i < count; i++)
            {
                var d = new DynamicParameters();
                d.Add("T1", $"A{i:0000}", System.Data.DbType.String);
                d.Add("T2", $"B{i:0000}", System.Data.DbType.String);
                d.Add("T3", $"C{i:0000}", System.Data.DbType.String);
                d.Add("T4", $"D{i:0000}", System.Data.DbType.String);
                d.Add("N1", i, System.Data.DbType.Int32);
                d.Add("N2", i, System.Data.DbType.Int32);
                d.Add("N3", i, System.Data.DbType.Int32);
                d.Add("N4", i, System.Data.DbType.Int32);
                d.Add("D1", DateTime.Today.AddDays(i));
                d.Add("D2", DateTime.Today.AddDays(i));
                d.Add("D3", DateTime.Today.AddDays(i));
                d.Add("D4", DateTime.Today.AddDays(i));
                data.Add(d);
            }
            TestDbExecute(data, true, false);
            TestDbExecute(data, true, true);
            TestDbExecute(data, false, false);
            TestDbExecute(data, false, true);
        }
 
 
staticobject sync = newobject();
 
staticvoid TestDbExecute(List<DynamicParameters> data, 
bool insert, bool parallel)
        {
string cmdText = insert ? insertCommand : updateCommand;
using (SqlConnection cn = new SqlConnection(cs))
            {
                Stopwatch sw = new Stopwatch();
                cn.Execute(truncCommand);
                sw.Start();
if (!parallel)
                {
foreach (var d in data)
                    {
                        cn.Execute(cmdText, d);
                    }
                }
else
                {
int threadCount = 0;
int maxThreadCount = 0;
                    Parallel.ForEach(data, (d) =>
                    {
lock (sync)
                        {
                            threadCount++;
if (threadCount > maxThreadCount)
                                maxThreadCount = threadCount;
                        }
using (var cnx = new SqlConnection(cs))
                        {
                            cnx.ExecuteReader(cmdText, d);
                        }
 
                        Interlocked.Decrement(ref threadCount);
                    });
                    Console.WriteLine("[MaxThreads={0}]", maxThreadCount);
                }
                sw.Stop();
                Console.Write("{0} {1}  {2}: {3:n0}ms\n",
                    data.Count, parallel ? "Parallel" : "Loop", 
                    insert ? "Insert": "Update", sw.ElapsedMilliseconds);
            }
        }
    }
}

找了一台內網的遠端 SQL 資料庫進行測試,從 1024 到 8192 四種筆數,使用 Parallel.ForEach 都節省近一半時間,成效卓著:

1024 Loop Insert: 8,372ms
[MaxThreads=10]
1024 Parallel Insert: 4,668ms
1024 Loop Update: 8,737ms
[MaxThreads=11]
1024 Parallel Update: 4,620ms

2048 Loop Insert: 16,665ms
[MaxThreads=14]
2048 Parallel Insert: 8,358ms
2048 Loop Update: 16,545ms
[MaxThreads=12]
2048 Parallel Update: 8,538ms

4096 Loop Insert: 36,444ms
[MaxThreads=22]
4096 Parallel Insert: 17,925ms
4096 Loop Update: 33,724ms
[MaxThreads=22]
4096 Parallel Update: 17,427ms

8192 Loop Insert: 67,885ms
[MaxThreads=31]
8192 Parallel Insert: 35,011ms
8192 Loop Update: 65,761ms
[MaxThreads=27]
8192 Parallel Update: 34,819ms

接著我改連本機資料庫執行相同測試,這一回加速效果很不明顯,甚至出現 Parallel.ForEach 比 foreach 迴圈還慢的狀況:

1024 Loop  Insert: 5,073ms
[MaxThreads=10]
1024 Parallel Insert: 4,772ms
1024 Loop Update: 4,342ms
[MaxThreads=10]
1024 Parallel Update: 4,457ms

2048 Loop Insert: 8,144ms
[MaxThreads=11]
2048 Parallel Insert: 8,672ms
2048 Loop Update: 8,540ms
[MaxThreads=12]
2048 Parallel Update: 8,659ms

4096 Loop Insert: 17,477ms
[MaxThreads=22]
4096 Parallel Insert: 17,860ms
4096 Loop  Update: 18,089ms
[MaxThreads=22]
4096 Parallel Update: 17,629ms

8192 Loop Insert: 33,393ms
[MaxThreads=30]
8192 Parallel Insert: 35,364ms
8192 Loop Update: 35,869ms
[MaxThreads=39]
8192 Parallel  Update: 36,817ms

比較上述兩組結果,Parallel.ForEach 更新遠端資料庫的時間與更新本端資料庫的時間相近,逼近資料庫的極限,可解釋為藉由平行處理排除網站傳輸因素後,遠端資料庫的效能表現趨近本機資料庫。平行處理的加速效應只出現在連線遠端資料庫,用在本機資料庫反而有負面影響,也能研判效能提升主要來自節省網路傳輸等待時間。

【結論】

在對遠端執行大量批次更新時,使用 Parallel.ForEach 確實能藉著忽略網路傳輸等待縮短總執行時間,在網路傳輸愈慢的環境效益愈明顯。既然效能提升來自避免等待,改用 ExecuteNonQueryAsync 應該也能產生類似效果,但程式寫法比 Parallel.ForEach 曲折些。這類做法本質偏向暴力破解,形同對資料庫的壓力測試,若條件許可,可考慮改用 BULK INSERTTVP等更有效率的策略。

【小插曲】

程式改寫 Parallel 版時,由於非同步執行進度不易掌握,使透過統計 ExecuteNoQuery() 傳回受影響筆數方式確認 Insert/Update 筆數無誤。原本預期不管是新增或修改,每次變更筆數都應該為 1,萬萬沒想到統計總數卻超過總資料筆數,貌似改為平行處理後執行結果不同,引發驚慌。

深入調查才發現:目的資料表掛有 Trigger, 在特定情況會連動其他資料表的資料,造成更新一筆但受影響筆數大於 1(要加上 Trigger 所異動的資料筆數)。 最後修改程式,改由受影響筆數 >0 判定是否執行成功,計數則一律+1,化解一場虛驚。

用 100 行 C# 打造 IP 所屬國家快速查詢功能

0
0

講到由 IP 地址查詢所屬國家,解決方案有兩種:第一種是直接呼叫線上查詢 API(付費或免費),再不然就要下載 IP 區段資料庫自寫查詢程式。考量應用場合不一定有 Internet 連線能力,加上擔心線上 API 無法滿足 IIS Log 等超大量 IP 解析的效能要求,選擇取回資料檔自幹。(其實是因為這題目大小難易適中,十分適合練功,一時手癢難耐,就…)

爬文找到一些 IP 國別對應資料來源:

最後決定選用 software77 的資料,看中它每日更新以及號稱 99.95% 的準確率。(各家資料格式大同小異,要更換來源並非難事,微調匯入邏輯即可)

由網站取回 IpToContry.csv 格式如下,前方有一大段註解直接略過即可。資料部分共有七欄,第1、2欄為 IP 區段的起始位址及結束位址(IP 位址不使用 aaa.bbb.ccc.ddd 字串格式,而是將四個 Byte 轉換為整數),第 5 欄為國別代碼,第 7 欄有國家名稱。

一般處理 IP 區段查詢,最常見做法是轉進資料庫後使用 SQL 查詢。評估資料筆數大約 17 萬筆,轉成物件陣列放進記憶體進行查詢對 C# 及當代電腦硬體是一碟小菜,不依賴資料庫的輕巧小程式更貼近我的開發哲學,用一個小類別打死才帥。

先不費太多腦力,用最直覺的 C# LINQ 來解:定義一個範圍物件 IPRange,屬性包含起始位址、結束位址、國別代碼、國家名稱。將資料檔轉為 List<IPRange>,查詢用 .SingleOrDefault(o => ip >= o.Start && ip <= o.End) 就能輕鬆達成, 50 行搞定:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
 
namespace IP2C.Net
{
publicclass RookieIPCountryFinder
    {
publicclass IPRange
        {
publicuint Start;
publicuint End;
publicstring CnCode;
publicstring CnName;
        }
 
        List<IPRange> Ranges = new List<IPRange>();
 
public RookieIPCountryFinder(string path)
        {
foreach (var line in File.ReadAllLines(path))
            {
if (line.StartsWith("#")) continue;
//"87597056","87599103","ripencc","1338422400","ES","ESP","Spain"
                    var p = line.Split(',').Select(o => o.Trim('"')).ToArray();
                    Ranges.Add(new IPRange()
                    {
                        Start = uint.Parse(p[0]),
                        End = uint.Parse(p[1]),
                        CnCode = p[4],
                        CnName = p[6]
                    });
                }
        }
publicuint GetIPAddrDec(string ipAddr)
        {
byte[] b = IPAddress.Parse(ipAddr).GetAddressBytes();
            Array.Reverse(b);
return BitConverter.ToUInt32(b, 0);
        }
 
publicstring GetCountryCode(string ipAddr)
        {
uint ip = GetIPAddrDec(ipAddr);
            var range = Ranges.SingleOrDefault(o => ip >= o.Start && ip <= o.End);;
if (range == null)
return"--";
else
return range.CnCode;
        }
    }
}

為了驗證結果,從 https://www.randomlists.com/ip-addresses取得 1024 筆隨機網址,以 http://www.ip2c.org/168.95.1.1方式查出國別,做好 1024 筆測試資料。以單元測試執行批次查詢與 ip2c.org 查詢結果進行比對,驗證結果是否一致。

測試前先觀察 ip2c.org API 執行速度方便比較,經實測一次耗時約 950 – 975ms。

執行單元測試,隨機 1024 筆資料查詢結果與 ip2c.org 查詢結果一致(綠燈),總查詢時間約 4.5 秒,換算每次查詢約 4.5ms,比呼叫 API 快 200 倍。

不過,LINQ 查詢固然直覺方便,若你以為它會像 SQL WHERE 查詢一樣有效率就錯了,恭喜跌入效能陷阱。如果講求效能,得換一顆更專業的查詢引擎。從已排序陣列找出指定數字落點,二分搜尋法是我心中的首選,原以為得捲袖子自已寫,卻發現 Array.BinarySearch在 .NET 已內建 ,哈里路亞!

配合二分搜尋,匯入資料結構也要調整,我的做法是將開始位址及結束位址轉成數字陣列,使用 Dictionary 對應國別碼。有個問題是範圍與範圍間可能存在未定義的空隙,發現範圍不連續時要補上一段開始、結束範圍指向未定義國別(國別碼填入"—"),才能精準回報未定義。另外,資料中有五個區段被重複定義指向兩個不同國家(實務可能發生,參見 FAQ),處理時也需排除。

查詢核心以 Array.BinarySearch 找出 IP 位址在已排序陣列的相對位置,若在陣列裡找不到該數字,BinarySearch 會傳回最接近位置的補數,轉換後可找到所屬範圍的位址。BinarySearch 版本範例如下,加上簡單的防錯,100 行搞定:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
 
namespace IP2C.Net
{
publicclass IPCountryFinder
    {
        Dictionary<string, string> CountryNames = new Dictionary<string, string>();
        Dictionary<uint, string> IP2CN = new Dictionary<uint, string>();
uint[] IPRanges;
publicstring DupDataInfo = string.Empty;
 
public IPCountryFinder(string path)
        {
if (!File.Exists(path)) thrownew ArgumentException($"{path} not found!");
string dupInfo = null;
            StringBuilder dupData = new StringBuilder();
uint lastRangeEnd = 0;
string unknownCode = "--";
            CountryNames.Add(unknownCode, "Unknown");
int count = 0;
try
            {
foreach (var line in File.ReadAllLines(path))
                {
if (line.StartsWith("#")) continue;
try
                    {
//"87597056","87599103","ripencc","1338422400","ES","ESP","Spain"
                        var p = line.Split(',').Select(o => o.Trim('"')).ToArray();
                        var st = uint.Parse(p[0]);
                        var ed = uint.Parse(p[1]);
                        var cn = p[4];
 
//range gap found
if (lastRangeEnd > 0 && st > lastRangeEnd)
                        {
//padding unknown range
                            IP2CN.Add(lastRangeEnd, unknownCode);
                            IP2CN.Add(st - 1, unknownCode);
                            count += 2;
                        }
 
                        dupInfo = $"{st}-{ed}-{cn}";
                        IP2CN.Add(st, cn);
                        IP2CN.Add(ed, cn);
                        lastRangeEnd = ed + 1;
if (!CountryNames.ContainsKey(cn))
                            CountryNames.Add(cn, p[6]);
                    }
catch (ArgumentException aex)
                    {
                        dupData.AppendLine($"Duplicated {dupInfo}: {aex.Message}");
                    }
                }
                IPRanges = IP2CN.Select(o => o.Key).OrderBy(o => o).ToArray();
            }
catch (Exception ex)
            {
thrownew ApplicationException($"CSV parsing error: {ex.Message}");
            }
            DupDataInfo = dupData.ToString();
        }
publicuint GetIPAddrDec(string ipAddr)
        {
byte[] b = IPAddress.Parse(ipAddr).GetAddressBytes();
            Array.Reverse(b);
return BitConverter.ToUInt32(b, 0);
        }
 
publicstring GetCountryCode(string ipAddr)
        {
uint ip = GetIPAddrDec(ipAddr);
int idx = Array.BinarySearch(IPRanges, ip);
if (idx < 0)
            {
int idxNearest = ~idx;
if (idxNearest > 0) idxNearest--;
                idx = idxNearest;
            }
return IP2CN[IPRanges[idx]];
        }
 
publicstring ConvertCountryCodeToName(string cnCode)
        {
if (CountryNames.ContainsKey(cnCode))
return CountryNames[cnCode];
return cnCode;
        }
 
publicstring GetCountryName(string ipAddr)
        {
return ConvertCountryCodeToName(GetCountryCode(ipAddr));
        }
    }
}

以相同資料重新測試 BinarySearch 引擎版本。

薑!薑!薑!薑~ 1024 筆只花了 9ms!!平均每一筆耗時 0.01 ms,比 LINQ 查詢版快了450 倍,比呼叫 API 快了 9 萬倍!速度快到嚇我一大跳,雖然大部分是 Array.BinarySearch 的功勞,但我很滿意。

完整程式及單元測試已放上 Github,有興趣玩玩的同學請自取。

呼口號時間:

C# 真棒,.NET 好威呀!

【茶包射手筆記】在 View 使用 SELECT * 的風險

0
0

分享同事踩到的 SELECT * 地雷一枚。

大家應該在程式設計準則都看過這條-「避免使用 SELECT * FROM Table,應以 SELECT Col1, Col2… 明確列舉欄位…」。

如此建議必有其考量:第一個理由顯而易見,正向表列必要欄位,可避免在網路傳送用不到的資料浪費頻寬,並能減少客戶端、伺服器端處理多餘資料的資源損耗。再者,查詢欄位多寡也可能影響效能,SELECT * 時為牽就非必要欄位,資料庫可能改用較無效率的索引,不利效能最佳化。還有一種極端情境,若所需欄位都存在於 Non-Clustered Index,即便使用的索引相同,SELECT * 迫使資料庫逐筆再 Key Lookup 取回完整資料列,拖累執行計劃並衍生非必要 IO。

另外,未正向列舉欄位在 Schema 變動時容易造成問題。例如:INSERT INTO TableA SELECT * FROM TableB,一旦 TableA 或 TableB 欄位增減或調換順序就會出錯。應寫成 INSERT INTO TableA (C1,C2,C3) SELECT C1,C2,C3 FROM TableB 才嚴謹。

以上均為我已知效能問題或嚴謹性疏失,但這次的案例讓我有些意外,是一個 View 使用 SELECT * 導致 Schema 一變更就爆炸的個案!

使用以下案例重現問題:

有一個具備 ID、NAME 兩個欄位的資料表 TBL9527,另外有 View VW9527,內容為 SELECT * FROM TBL9527。塞入一筆資料並查詢 VW9527,結果符合預期:

修改 TBL9527,在 ID 與 NAME 欄位間插入一個 TEAM 欄位。

猜猜怎麼了?SELECT * FROM TBL9527 結果正常,但 VW9527 查詢結果的 NAME 對應成 TEAM,内容為 NULL!

要修正問題,需執行 EXEC sp_RefreshView'VW9527' 更新 View 的 Metadata。

咦~ 在 View 使用 SELECT * 的朋友愛自意哦!

【參考資料】

ActionFilter Attribute 共用特性與狀態保存

0
0

同事報案,某個 Web API 會不定期出錯。進一步調查是近期啟動的一個新排程同步發出多個 API 呼叫,當 Web API 同時被多方呼叫,Web API 加掛用來寫 Log 的 ActionFilter Attribute 偶爾會發生 Dictionary.Add 重複加入相同鍵值的錯誤。因 Dictionary 被設成 ActionFilter Attribute Instance 的私有欄位,依我原先的理解,ActionFilter Attribute 每次呼叫時都應建立新 Instance,不致因共用打架,但觀察結果顯然與假設不符。進一步檢查 Log 軌跡,確實找到兩次 Request 共用 ActionFilterAttribute 覆寫變數內容的證據!由此,為什麼只在 Instance 執行一次 Dictionary.Add 卻發生鍵值重複便有了合理解釋。

爬文才發現,自 ASP.NET MVC3 起,Action Filter 更積極藉由 Cache 機制重複使用,不再每次 Request 重新建立:參考

In previous versions of ASP.NET MVC, action filters are create per request except in a few cases. This behavior was never a guaranteed behavior but merely an implementation detail and the contract for filters was to consider them stateless. In ASP.NET MVC 3, filters are cached more aggressively. Therefore, any custom action filters which improperly store instance state might be broken.

用以下實例重現問題。用 ActionFilterAttribute 做一個超簡單的執行耗時顯示,在 OnActionExecuting() 以物件欄位記錄 p 參數及開始時間、OnResultExecuted() 計算並顯示耗時及 p 參數:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace MVCLab.Models
{
publicclass ExecTimeAttribute : ActionFilterAttribute
    {
string id;
        DateTime start;
publicoverridevoid OnActionExecuting(ActionExecutingContext filterContext)
        {
            id = filterContext.HttpContext.Request["p"] ?? "NULL";
            start = DateTime.Now;
        }
publicoverridevoid OnResultExecuted(ResultExecutedContext filterContext)
        {
            var dura = DateTime.Now - start;
            filterContext.HttpContext.Response.Write($"<span>{id}: {dura.Milliseconds}ms</span>");
        }
    }
}

在 HomeController.cs Index Action 加上 [ExecTime] Attribute:

using MVCLab.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
 
namespace MVCLab.Controllers
{
publicclass HomeController : Controller
    {
        [ExecTime]
public ActionResult Index()
        {
            var rnd = new Random();
            Thread.Sleep(rnd.Next(500));
return View();
        }
    }
}

Index.cshtml 長這様:

@{ Layout = null; }
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Index</title>
</head>
<body>
<div>測試(@Request["p"])</div>
</body>
</html>

寫個 Test1.html 做測試:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8"/>
<style>
        iframe { width: 150px; height: 60px; margin: 3px; float: left; }
</style>
</head>
<body>
<inputid="txtUrl"value="/Home/Index?p=001"/>
<buttononclick="test()">Test</button>
<br/>
<iframeid="frmTest"src="about:blank"></iframe>
<script>
function test() {
            document.getElementById("frmTest").src = document.getElementById("txtUrl").value;
        }
</script>
</body>
</html>

測試結果符合預期:

問題發生在多個 Request 並行時。用三個 IFrame 一次發出 3 個 Request,參數分別傳入 001、002、003:

<!DOCTYPEhtml>
<html>
<head>
<title></title>
<metacharset="utf-8"/>
<style>
        iframe { width: 150px; height: 60px; margin: 3px; float: left; }
</style>
</head>
<body>
<iframesrc="/Home/Index?p=001"></iframe>
<iframesrc="/Home/Index?p=002"></iframe>
<iframesrc="/Home/Index?p=003"></iframe>
</body>
</html>

然後它就壞掉了!下方顯示的 p 參數全部被覆寫成 003…

由此可知,使用 Attribute 物件屬性或欄位保存狀態可能因物件重複使用導致資料覆寫,故較好的做法是將狀態改存入專屬每個 Request 的 HttpContext,TempData 是不錯的選擇。修改後程式如下:

using System;
using System.Web.Mvc;
 
namespace MVCLab.Models
{
publicstaticclass MyContextExt
    {
publicstatic T GetVar<T>(this ResultExecutedContext ctx, string varName)
        {
return (T)ctx.HttpContext.Items[varName];
        }
publicstaticvoid StoreVar<T>(this ActionExecutingContext ctx, string varName, T data)
        {
            ctx.HttpContext.Items[varName] = data;
        }
    }
 
publicclass ExecTimeAttribute : ActionFilterAttribute
    {
//REF: http://stackoverflow.com/a/8937793/4335757
publicoverridevoid OnActionExecuting(ActionExecutingContext filterContext)
        {
            filterContext.StoreVar<string>("id", filterContext.HttpContext.Request["p"] ?? "NULL");
            filterContext.StoreVar<DateTime>("start", DateTime.Now);
        }
publicoverridevoid OnResultExecuted(ResultExecutedContext filterContext)
        {
            var dura = DateTime.Now - filterContext.GetVar<DateTime>("start");
            var id = filterContext.GetVar<string>("id");
            filterContext.HttpContext.Response.Write($"<span>{id}: {dura.Milliseconds}ms</span>");
        }
    }
}

改用 TempData 保存狀態後,程式運作正常,問題排除。

好問題:GUID 真的不會重複嗎?

0
0

前幾天,「系統產生的 GUID 是否可能發生重複?」在辦公室引起熱議。我主張:GUID 透過網卡 MAC 地址、產生時間以及一些機制(防止同時間點產生兩筆或時鐘往回調)確保世上任何電腦都不會產生相同 GUID,只要所有電腦的 MAC 地址沒有亂來,理論上不可能發生重複。這說法挺有說服力,解除了大家心中的疑慮。

BUT,禁不住好奇爬了文,這才發現「我錯了!」

倒不是不該信任 GUID 永不重複,而是我們現在使用的 GUID 早已不是依據 MAC 及時間產生,而是靠隨機亂數產生。

GUID(Globally Unique Identifier)是微軟依據 UUID(Universally Unique Identifier)規範實作的唯一識別碼。至於 UUID 的演算法主要有四種:(Version 2 極罕用,直接忽略)

Version 1

使用 Timestamp 與網卡 MAC 網址確保唯一性,這就是我原本認知的做法。Version 1 將結構分成:

  1. 60 位元的時間戳記(Timestamp)
  2. 4 位元的演算法識別碼,固定為 0001(Algorithm 1),只要各演算法保證自己不產生重複值,不同演算法間也不會重複
  3. 14 位元的緊急識別位元(Emergency Uniquifier Bits)
    由於 UUID 依賴時間產生唯一性,當同一時間要產生多筆 UUID 時或時鐘被調回過去,藉著會遞增的 Uniquifier 確保時間相同 UUID 也不致重複
  4. 2 位元保留值,固定為 01
  5. 48 位元為產生該 UUID 電腦的識別碼,通常使用網卡的 MAC 地址;若沒有網卡,則第一個位元設為 1其餘 47 位元為亂數。(網卡 MAC 的第一位元永遠為 0,故不會跟無網卡的亂數識別重複)

60 + 4 + 14 + 2 + 48 = 128 位元,完整 UUID 是唯一的,但只擷取其中部分就不保證。

Version 3 & Version 5

依據 Namespace 及 Name 字串雜湊碼(Vesrion 3 用 MD5、Version 5 用 SHA1)產生,固定輸入將產生一致的 UUID。

Version 4

由 4 位元版號及 2位元 UUID Variant 加上 122 位元亂數產生,主要靠 Pseudorandom Number Generator(PRNG,仿隨機數字產生器)產生具有唯一性的亂數(但不保證不可預測性)。

Version 1 就是我小時候學過 GUID 保證不重複的原理依據。不過,因為有洩露 MAC 地址的安全疑慮(當年這個漏洞還被用來追查梅杜莎病毒來源),微軟從 Windows 2000 起便改用 Version 4 ,以隨機亂數產生 GUID:參考

With Windows 2000, Microsoft switched to version 4 GUIDs, since embedding the MAC address was viewed as a security risk.

不信?你可以產生一堆 NewGuid() 試試,看看第三節數字的第一位是否永遠為 4?這個 4 即代表它是 Version 4 的 UUID!

 

【結論】

好,所以現在我們知道了!GUID 已不是靠 MAC 地址及時間確保唯一性,而是靠隨機亂數產生,靠著 122 位元數夠多讓重複機率趨於0。那那那,GUID 有沒有可能重複?當然有可能,但機率有多低呢?依據維基百科,預估的機率是 2.71 * 10^18 分之一,若每秒產生 10 億個 UUID 連續 85 年,將有 50% 的機率至少發生一次重複。如果你很想目睹 GUID 強碰,必須產生 103 百萬兆個 UUID,才會有 10 億分之一的重複機率

這下麻煩了,只是機率極低而非全無可能,那我們可不可以說「GUID 永遠唯一」?

我的看法是「因人而異」!

如果你常擔心走在路上會被殞石爆頭,再不然就是明天有外星艦隊攻打地球,常煩惱要是回家路上撿到彩券中樂透,該不該馬上辭掉工作四海雲遊?那麼你應該不介意再多操煩一件芝麻小事:「靠北!要是 GUID 重複系統崩潰該怎麼辦?」。

不然的話,我建議大家學我一樣,別想太多,大聲說「別擔心,GUID 永遠不重複!」就好。

要是真遇上了,我誠心道歉並恭賀閣下獲得老天爺對人品至高無上的肯定。

【參考資料】

WinDBG 應用實例:找出 ASP.NET CPU 100% 原因

0
0

故事是這様的,我們有一批網站搬到新主機出現詭異現象:每隔一段時間某些 IIS AppPool Process 會吃滿 25% CPU 使用量,在 4 核機器這象徵有一條 Thread 陷入無窮迴圈吃光一個 CPU Core 的時間。有時也會出現多個 AppPool 同時發難,每個 Process 吃 25%,把整體 CPU 使用率逼上 50%、75%,甚至 100%。出問題時,該 AppPool 網站仍能使用,但無法透過 IIS 管理回收 AppPool,只能用 TaskManager 砍掉 Process。砍掉 Process 後,系統會乖一陣子,但幾個小時或隔天又復發。

原本正常的程式移到新環境不穩定讓人心慌,很想知道吃光 CPU 的 AppPool 究竟在忙什麼?到底是哪支網頁是老鼠屎?我唯一想到的可用武器是強大但陌生的 WinDbg (Windows Debugger),爬文與一番摸索後居然真讓我成功抓出問題所在,興奮到想轉圈灑花。:P

以下就分享本次使用 WinDbg 找出 CPU 100% 程式來源的經驗:

  1. 使用 TaskManager 產生 Memory Dump 檔
    找出吃光 CPU 的 w3wp.exe Process,叫出右鍵選單執行「Create dump file」 產生記憶體傾印檔。

    有件事要注意,要先釐清 AppPool 是 32 位元還是 64 位元,若為 32 位元需改用 32 位元版 TaskManager,否則 Dump 檔解析出來的資訊會類似 wow64cpu!TurboDispatchJumpAddressEnd+0x598,看不到真實執行位置。32 位元版 TaskManager 位於 C:\Windows\SysWOW64\taskmgr.exe,執行後 Process 名稱應為「Task Manager (32bit)」。

  2. 啟用 WinDbg 載入 Dump 檔案
    Windows Debugger 有 32/64 位元之分,請視要分析對象的位元版本選擇對應版本的 WinDbg。
    啟動 WinDbg 後,使用 File / Open Crash Dump 開啟剛才取得的 w3wp.DMP。

    註:Windows Debugger 下載安裝資訊請自行參考 MSDN,此處不多贅述。
  3. WinDbg 的指令既多且雜,有興趣的朋友請參考文件,此處記錄並說明我動用的指令:
    .sympath srv*X:\Symbol*https://msdl.microsoft.com/download/symbols
    分析過程需要 Symbol 檔,指定 WinDbg 自動由微軟網站下載,並 Cache 在 X:\Symbol 目錄避免重複下載
    !sym noisy
    指定顯示完整 Symbol 下載資訊
    .cordll -ve -u  -l
    自動載入 CLR 偵錯相關模組(分析來自其他台機器 Dump 檔時,這招好用) 
  4. 找出佔用 CPU 最多的 Thread,並將偵錯對象切換成該 Thread
    !runaway
    找出每個 Thread 消耗的 CPU 時間,在本案例中,Thread 27 耗用超過一個小時,無疑是吃光 CPU 的兇手
    ~27s
    將偵錯對象切換成 Thread 27

  5. 使用 !clrstack 列出 Thread 27 的 Callstack

兇手現形!MasterPage 中某個共用元件使用 ODP.NET 讀取使用者基本資料,程式卡在 Oracle.DataAccess.Client.OpsSql.ExecuteReader(),有可能等待 Oracle 資料庫回應時陷入無窮迴圈並耗盡 CPU。

陸續分析了幾起案例的 Dump 檔,都能找到一條 Thread 明顯耗用大量 CPU,而 Callstack 問題都指向 Oracle.DataAccess.Client.OpsSql.ExecuteReader(),鎖定問題後推測是網路設備干擾 Oracle 連線的問題(先前看過的症狀都是收到明確錯誤,像這樣 Hang 住的狀況是第一次遇到),避開後問題排除。

就這樣,靠著 WinDbg 漂亮偵破一個看似毫無頭緒的疑案。

呼口號時間:

WinDbg 好威呀!


ASP.NET CPU 飆高問題之傻瓜分析工具-DebugDiag Tools

0
0

昨天使用 WinDbg 追查 ASP.NET CPU 100% 原因的文章得到不少朋友的回饋,其中 Robert Hu 留言提到一個更方便的 Dump 擷取與問題分析工具,試用之下果然犀利,在此補上介紹。

Debug Diagnostic Tool (DebugDiag) 是微軟針對程式當掉(Crash)、當住(Hang),以及記憶體洩漏(Memory Leak)等問題設計的快速偵察工具,目前最新版為 Debug Diagnostic Tool v2 Update 2,共有三項兵器:

  1. DebugDiag 2 Collection

    自動化蒐集 Dump 檔的工具,可針對不同情境指定抓取 Dump 時機,例如:程式拋出非預期的例外、效能計數器達到門檻、HTTP 回應時間過慢、監測記憶體用量改變… 等,功能強大。


    DebugDiag Collection 最強悍之處在於設定規則守株待兔,當程序 CPU 使用量或其他效能指標超過門檻時匯出 Dump 檔(注意:匯出 Dump 期間程序會暫停,將影響線上使用者),對付「ASP.NET 不定期 CPU 飆高」之類的問題格外有用!
    下圖範例即為主機 CPU 使用百分比超過 30% 連續五秒時產生一到多個 AppPool Process 的 Dump 檔,有助於抓出耗用 CPU 的網站程式。
  2. DebugDiag2 RuleBuilder

    若 DebugDiag 2 Collection 預設抓取規則不夠用,Rule Builder 支援以視覺化流程圖方式自訂規則。
  3. DebugDiag 2 Analysis

    自動 Dump 分析工具,針對當機、當住及記憶體洩漏等情境預先寫好分析邏輯,輸入一個或多個 Dump 檔後自動產生分析報表。上回說到的 ASP.NET CPU 100% 問題,使用 PerfAnalysis 可以瞬間找到答案。

操作方法很簡單,如上圖所示,按下 Start Analysis,選好 Dump 檔,DebugDiag Analysis 就開始分析,稍待約一分鐘(運算時間長短視 Dump 大小、數量、分析項目而定)熱騰騰的分析報表網頁就出爐了。在報表尋找 Analysis Details /PerfAnalysis / Performance Summary / Statistical Rollups / Top 1 Operations By Duration,相當於用 WinDbg 尋找耗用 CPU 最久的 Thread 的分析資料,而共用元件呼叫 ODP.NET OracleCommand.ExecuteReader 的 Callstack 資料就在眼前。

使用 DebugDiag Analysis,只需幾個 Click 就能得到與 WinDbg 一連串指令操作相當的結果。若 WinDbg 是傳統單眼,DebugDiag Analysis 就像傻瓜相機,管它什麼快門調多快光圈開多大,按下快門就有好照片,還附贈美肌效果,神奇無比。

至於這兩種工具的選擇,若目的在檢視問題 Thread 當下的 Callstack,要「快」請用 DebugDiag Analysis,要「帥」就用 WinDbg 吧!哈。

Oracle 追討 Java 授權費議題之研究心得

0
0

前陣子有 Oracle 對企業追討 Java 授權費的新聞搞得人心惶惶:

被很多人問到「為了跑 Java 程式裝 JRE/JDK 也會被收錢嗎?」「裝什麼版本才會被收錢?」…

身為 Java 麻瓜,我知道才有鬼,新聞寫得不是很清楚,自己也好奇有無方法排除侵權疑慮,故門外漢爬文整理心得如下。(聲明:對此領域全然陌生,純為爬文心得拋磚引玉,如有錯誤歡迎補充指正)

  1. Java 產品很多,Oracle 官方網頁列舉的 Java 產品系列: 參考來源:PTT
    *Java SE (最通用的一般平台)
    *Java EE (企業級,例如:Serverlet)
    *Java ME (行動平台,已漸漸勢微)
    *Java SE Support (顧問服務)
    *Java SE Advanced & Suite (SE的擴增功能、更強大的SDK… 等)
    *Java Embedded、Java DB、Web Tier、Java Card、Java TV
    Java EE/ME 需購買授權眾所周知,最有爭議的是 Java SE,也就是一般 PC 跑 Java 程式要去 Oracle 網站下載的版本。
  2. Java SE(JRE、JDK)是免費的。BUT!在一些情境下需購買授權: 參考
    *用於「智慧系統中的專門嵌入式電腦」(specialized embedded computers used in intelligent systems)
    *透過工具來大規模部署開發完成的應用,例如:Windows Installer Enterprise JRE Installer工具」,會超出「一般運算用途」(general purpose computing)
    *使用 Advanced Desktop、Advanced、Suite 等進階商業付費功能
    前兩者情境很好識別,但第三種情境就有點模糊空間,甚至像是陷阱。

  3. 進階商業付費功能變成陷阱的原因在於 Oracle 沒有為 Advanced Desktop、Advanced、Suite 拆出獨立安裝程式,全部包在 Java SE 標準安裝裡,就算你不想用,一旦下載安裝,電腦就具備使用 Advanced Desktop、Advanced、Suite 的能力,啟動 JVM 時加上 –XX:+UnlockCommercialFeature 參數即可開啟。
    實際觀察,Java SE Adavnced 跟 Suite 的下載連結就是指到 Java SE 標準版的下載連結,應可證明 Oracle 並未區隔出不同的安裝程式。

    另外,我也找到一篇國外討論也提到 Oracle 沒有針對免費及付費產品提供不同安裝程式的問題:
    "Java SE is free for what Oracle defines as “general purpose computing”...But it is customers in these general-purpose settings getting hit by LMS. The reason is there’s no way to separate the paid Java SE sub products from the free Java SE umbrella at download as Oracle doesn’t offer separate installation software"
    這讓我想起餐廳「還沒點菜桌上就擺了幾盤小菜,等著你一挾就得付錢」的小伎倆,合法但存在爭議。
    在沒有選擇的情況下安裝了付費功能,變成使用者得證明自己沒用不需付費,稱之為陷阱也不為過。

  4. 一般使用者自然不會知道 –XX:+UnlockCommercialFeature 這種鬼參數,只知道點兩下程式就會動了。若使用者下載了第三方提供的 Java 程式,開發者在啟動程式批次指令裡下了 –XX:+UnlockCommercialFeature 參數… 其實我搞不懂,這樣子是開發者要付費還是使用者要付費?
    付費功能是使用者安裝的沒錯,但使用者可主張自己從沒打算使用(其實是被硬塞的),封印被第三方程式解除,此時該算誰的責任是個好問題。
    這有點像在手機裝了某個 App 在亂發簡訊,帳單得自己繳沒話說,但手機裝 App 前會有明顯的要求簡訊發送的授權確認,Java SE 進階功能在啟用時少了明確親民的提示,就易惹來爭議。

新聞裡有提到:

不少企業已經請授權專家和 Java 專家來檢視自家的 Java 版權。專家建議,當使用者下載 Java 就會得到所有的元件,因此必須要確認你只安裝那些需要且符合授權的元件,並刪除不需要的元件。

但依據上述研究心得,沒授權但不想用元件會被包在 Java SE 一起被裝起來,要怎麼刪除是個謎。而 Oracle 為什麼把事情搞得這麼複雜?把 Java SE 安裝程式分成免費版跟商業版,很難嗎?

最後,若想徹底遠離扯不清的 Java SE Advanced Desktop、Advanced、Suite 授權爭議,有幾個辦法:

  • 如果完全用不到 Java 程式,可以移除 Java SE 製造不在場證明
  • 改用 OpenJDK吧!開源萬歲~

2017 渣打馬拉松與 SUB 4 之夢

0
0

跑馬當踏青,荒唐一整年,總得有場玩真的,渣打馬就是你了!

去年渣打馬遇到連貓空都下霰的霸王級寒流,今年雖是暖冬,渣打馬照例再跟入冬最冷寒流強碰,原本預測將到 10 度以下,最後是 11 度的乾冷天氣。老天爺送上大禮,加上近來無傷無痛,這様還破不了 PB(個人最佳成續)豈不人神共憤,天地不容?

賽前我還很假掰認真地參考 RunningQuotient分析報表調整跑量,設法拉高狀況指數(狀況=體能-疲勞),破 PB 勢在必得。

我的 PB 是 2015 年國道馬創下的 4 小時 15 分,之後十來場都在吃喝玩樂,視成績如浮雲。近半年體重維持不錯加上核心肌群練得勤,去年入冬天氣轉涼後相同配速的心率明顯下降。晨跑 5K 心率 M 區間(HRR 80-88%) 可以跑出 Pace 5:10-5:30,破 PB 情勢大好。只是,之前兩次妄想扣關 SUB4 的經驗告訴我,能維持目標配速穩穩跑完 30 K 不等於有本事撐到完賽,35K 後兵敗如山倒的滋味難以忘懷。即便近況不錯,感覺 SUB4 仍是遙不可及的目標。

清晨四點半出門,拜 SuperDry 極度乾燥之賜,體感溫度不如預期冷,是好兆頭。會場離家算近,不用半小時就抵達總統府,算是天時地利,感覺破 PB 的希望又更濃了。

這張照片說明我熱愛小而美賽事的理由:

起跑前來一張,拼 PB 專心跑都來不及,就乖乖把手機收起來不拍照了,不留給自己任何「我是邊跑邊拍所以才…」的爛藉口。

在意成績,刻意排在前面一點想多少節省超車時間,另一方面前段跑友平均實力略強,寄望靠「同儕壓力」逼出潛能。這場比賽有好多 Garmin 全馬 PB 班同學穿著醒目的紫色漸層背心,個個實力堅強(即使女跑友亦然),是很棒的假想敵。

上屆四小時內上五次廁所搞到信心崩盤自暴自棄,參考這陣子天冷練跑的補水經驗,決定每個水站只喝一杯,避免喝多頻尿悲劇重演。這次全程只上了一次廁所,算是策略執行成功。至於配速,跟教練團(咦?)討論後,決定回歸 Follow Your Heart 的 HRDR(Heart Rate Driven Run),心率維持在平日最習慣的 M 區間不爆衝,預期配速應可自然落在平日晨跑慣用的 5:20。

1-10K: 05'57"/05'21"/04'58"/05'11"/05'13"/05'23"/05'11"/05'50"/05'11"/05'16"
起跑後人群洶湧,但仍能拉到 6 分速,排在前段多少有幫助,第二公里後就能依自己預期的速度前進,前 10K 花了 53分跑完。

11-20K:05'13"/05'16"/05'10"/05'24"/05'08"/05'19"/05'04"/05'42"/05'11"/05'16"
11K 起狀況維持得不錯,配速都在 5'15" 左右,只有一次停水站吃了幾塊餅乾多花了 30 秒,20K 只花 1 小時 46 分。

21-30K:05'18"/05'24"/05'06"/05'19"/05'24"/05'24"/05'29"/05'28"/05'35"/5'31"
前 21K 跑了 1:52:02,破了半馬 PB,心情大好,但身體很誠實地說「我快沒力了…」,速度開始下滑至 5'30",前 30K 2 小時 40 分完成,繼續刷新記錄。

31-40K:05'18"/05'39"/05'41"/05'48"/05'42"/05'43"/05'43"/06'19"/05'35"/05'55"
最恐怖的 30K 大魔王來了!果不其然,速度再下滑到 5'40", 36K 起大腿開始硬化,感覺已站上抽筋邊緣,將速度放慢緩和一陣子抽筋感才消失,但已無力加速。但看看時間擦了擦眼睛,有點不敢相信,這場真的有機會 SUB 4 了,只求最後不要抽筋失速,

41-42K:06'02"/05'46"/05'41"
使出吃奶力氣,咬牙拖著僵硬的兩腿跑完最後兩公里… 天啊,第一次跑完全馬計時器還在 3 字頭,SUB 4!我辦到了!

沒有想像中的熱淚盈眶,心情甚至不如前兩次扣關失敗激動,就這樣平靜地跨過自己跑馬生涯的新里程碑。

當年沒破四就偷買先用的 Fenix 3,今天起總算可以戴得名正言順了,哈!

晶片成績 3:55:05,還意外突破傳說中的「陳冠希障礙」!(不過,接下來鐵腿兩天 Orz 證明拎杯真的盡全力了)


照片來源:http://sports.qq.com/a/20151216/012453.htm

垂垂老矣的馬拉松五年級生,SUB 4 完成!

公告:Facebook 粉絲專頁破萬抽獎活動

0
0

剛圓了 SUB4 之夢,部落格也悄悄跨過另一座里程碑-黑暗執行緒臉書專頁按讚數破萬了!(轉圈灑花)
PS:咦?好像有人衝去退讚了… 哼!不管!拎杯不在乎天長地久,只在乎曾經擁有。

為了紀念這個小小成就,還記得以前辦過的五百萬人次紀念抽嗎?是的,黑暗執行緒萬讚紀念抽來了!

先介紹本次抽獎獎品-薑!薑!薑!薑~獨一無二但毫無價值的「黑話(黑大說過的話)紀念書籤」,全球限量推出三枚,由黑大親手設計與製作:

(謝謝兩位臨演,可以下去領 500了)

書籤為廉價飛機木板高質感原木素材經 3D 雷射蝕刻加工而成,書籤收錄的金句字字珠璣,在您查不到Bug重開沒效找不出問題,人生失意徬徨迷惘手足無措之際,為您點亮一盞明燈… 以上都唬爛的,其實就只是有趣的紀念品而已。第一次玩雷射雕刻,這三枚書籤搞了我好幾天,燒了一堆瑕疵品,工藝水平不怎樣,但保證誠心打造。

我辦的抽獎當然要用我心中最公平公正公開的 Open Source 電腦抽獎法-程式演算法與亂數種子取碼規則預先公開,且需支援反覆驗證。懶得重想,直接借用上次五百萬紀念抽的程式,只微調最後一段改成抽出三位得獎者。Online Demo

using System;
using System.Collections.Generic;
using System.Linq;
 
publicclass Test
{
publicstaticvoid Main()
    {
string raw = @"1.Jeffrey
2.Darkthread
3.球證
4.旁證
5.主辦
6.協辦
7.全都是我的人";
            List<string> candidates = new List<string>(raw.Split('\n'));
            Random rnd = new Random(975047);
            Console.Write(
string.Join("\n", 
                candidates.OrderBy(o => rnd.Next()).Take(3).ToArray()));
    }
}

至於亂數種子就取 D 日的台股收盤指數,以 2/24 為例,台股加權指數為 9,750.47,亂數種子就取 975047:

要如何參加抽獎呢?請大家在臉書抽獎貼文留言「抽」或「+1」就可以囉~(以臉書留言為準,部落格文章留言恕不計入)

報名截止時間(D日T時)使用以下 JavaScript 取出名單,另排序以「最新動態」為準:

 

var dupCheck={}, list=[], idx=0;
document.querySelectorAll(".UFIList .UFICommentActorName").forEach(function(span) { 
var id=span.getAttribute("data-hovercard").match(/id=(\d+)/)[1];
var name=span.innerText; 
if (id in dupCheck || name == "黑暗執行緒") return;
  dupCheck[id]=name;
  list.push(idx++ + "." + name + "(" + id + ")");
});
console.log(list.join("\n"));

將名單送入程式,輸入 D 日台股指數當亂數種子,三位得獎就出爐囉~

好了,祝大家幸運中獎!

PS1:獎品很鳥,就不請律師公證了,大會保留任意修改及解釋抽獎規則的權利(靠,是有沒有這麼蠻橫?),如果擔心規則不公權益受損,請勿參加以免受氣。

PS2:中獎者請透過粉專訊息跟我連絡,由於獎品將採郵寄方式,依慣例若中獎者住在海外、外星球或其他銀河系,恕只能代寄到指定的台灣住址。

使用 Process Explorer 查看 .NET Callstack

0
0

WinDbg 追查 CPU 飆高問題一文發表後,在 FB 收到網友 Webber Han 回饋(在此感謝),提到射茶包利器 Process Explorer也能像 WinDbg 一樣檢視 Callstack 中的 .NET 組件、函式資訊,查了一下,這是 2012 年 15.2 版就加入的功能,Lag 大了。

關鍵在於「Configure Symbols」有無設定妥當,Process Explorer 的 .NET Callstack 解析也是借助 WinDbg 完成,故機器要先裝妥 WinDbg,開啟 Process Explorer 選單 Options / Configure Symbols… 將 Dbghelper.dll 改指向 WinDbg 的 dbghelper.dll,並比照 WinDbg 設定 Symbols path:

我寫了一支執行數十萬次 JsonConvert.SerializeObject 的 ASP.NET 程式當成測試對象。開啟 Process Explorer 找耗用大量 CPU 的 w3wp.exe,點兩下開啟 Properties,切換到 Threads 頁籤,找出佔用 CPU 最多的 Thread,點選 Stack:

此時 Stack 資訊可看出 ASP.hang_asp.Page_Load() 呼叫 HangTest.dll TestHelper.RunTest() ,RunTest() 呼叫 RunJsonTest(),其中再呼叫 JsonConvert.SerializeObject() 的呼叫關聯。

如此便能即時檢視執行中 .NET 程序的 Callstack,快速鎖定問題來源,在調查程式當住(Hang)問題時又多了一項選擇!

TIPS-以不同使用者身分執行程式

0
0

在一些情境下,我們需要切換成其他使用者身分執行程式,例如:以 UserA 登入 Windows,因特殊需求改用 UserB 帳號啟動特定程式。一個經典範例是 SSMS,如下圖所示,當選擇「Windows Authentication」認證方式,Username 欄位固定為當下登入帳號,無從改變。

要改變 SSMS 中的 Windows Authentication Username,就必須改用其他使用者身分執行 SSMS。Windows Vista 起有個內建命令列工具 Runas能指定執行身分,但找出程式路徑還有敲指令,有點費事:

runas /netonly /user:domain\user "C:\Program Files (x86)\Microsoft SQL Server\120\Tools\Binn\ManagementStudio\Ssms.exe"

最近我才發現,Windows 7 起可直接透過右鍵選單「以不同的使用者身分執行」(Run as different user),方法是按著 Shift 再按滑鼠右鍵,「以系統管理者身分執行」下方就會多出「以不同的使用者身分執行」。

跟連線網路磁碟機一樣,Windows 會彈出登入對話框,輸入帳號密碼完成驗證就能以其他使用者身分執行程式囉~

在 TFS 2012 Build Service 編譯 VS2015 專案

0
0

工作環境用的是 TFS 2012 Build Service,最近要編譯 VS2015 專案,程式用到 C# 6.0 超好用的字串插值寫法當場被打臉,得到 Unexpected character '$' 錯誤。原因很明顯,VS2015 改用 Roslym 編譯器,TFS 2012 Build Service 上沒裝是要編個屁?

經過一番摸索(還學到 TFS 的編譯範本原理)終於搞定,細節整理如下。

首先要下載 MS Build Tools 2015安裝到 Build Service 主機。(安裝完整 VS2015 應該也成,但有殺雞用牛刀之嫌)

安裝完畢 Program Files (x86) 目錄會多出 14.0 資料夾,這個路徑稍後會用到。

建置定義編輯畫面的 Process 區有個 Build process template 項目,預設是收合的,打開可以目前使用 Default Template (DefaultTempalte.11.1.xaml),而它位於 TFS 版控該專案的 BuildProcessTemplates 資料夾(每次建立 TFS 專案都會看到系統自動產生 BuildProcessTemplates 目錄,至今才明白它的用意)。這個 XAML 檔是一套 Windows Workflow 流程,最棒的是可直接用 Visual Studio 視覺化編輯。由於 DefaultTemplate.11.1.xaml 適用 VS2012/VS2013,需要修改才能用於 VS2015 專案。修改此範本使用其支援 VS2015 是種解法,而更好的做法是另外新增一個 VS2015 專用的建置範本,二者並存,未來再視 VS2013 或 VS2015 專案選擇適用範本。

其實 DefaultTemplate.11.1.xaml 只要小改路徑設定就可以編譯 VS2015 專案,我複製一份並改名為 DefaultTemplate.VS2015.xaml 放在 BuildProcessTemplates 目錄下,點擊兩下即可使用 Visual Studio 修改流程。在流程圖中找到「對專案執行MSBuild」:

開啟其屬性設定找到 ToolPath 項目填入我們安裝 MS Build Tools 2015 的路徑:"C:\Program Files (x86)\MSBuild\14.0\Bin":

此處有個眉角-XAML 裡還有另一個「對專案執行MSBuild」,也要修改:

將 XAML 存檔並簽入 TFS,接著將建置定義的建置範本換成 DefaultTemplate.VS2015.xaml(若它不在下拉選單中,請使用 New 選取加入)重新執行建置,就可以在 TFS 2012 Build Service 成功編譯 VS2015 專案囉~


查詢 SQL Server 詳細版本資訊

0
0

資安更新作業需確認 SQL Server 詳細版本資訊,需細到 Service Pack 及累積安全更新,爬文查到超好用的 SQL Server 版本資訊偵測腳本。這組自動偵測腳本由微軟OneScript Team提供,可顯示版號、產品名稱、產品層級(Service Pack 版本)、版本別(Express、Enterprise、Developer…)、32 / 64 位元、Service Pack 版本、累積更新(Cumulative Update)等資訊。

更厲害的來了,偵測結果還包含後續若要更新,最新版 GDR(General Distribution Release)及 QFE(Quick Fix Eningeering)的下載網址。

【小辭典】

SQL Server 更新的種類超多,順手整理如下: 參考

  • QFE,Quick Fix Engineering,也常被稱為 Hotfix
    較未經過大量測試的修正,經索取才提供(留下 Email 才寄立送連結,也方便事後發現問題通知),通常用於較緊急狀況。
  • COD,Critical On-Demand
    針對影響嚴重、範圍層面廣或安全相關問題所出的緊急更新,通常會收納到下次 CU/SP,COD 有時會包含多個 QFE。
  • CU,Cumulative Update
    在 SP 與 SP 之間的更新,通常以 60 天為單位匯集發行,測試完整度不如 SP,安裝到正式環境前應先測試,但微軟鼓勵積極更新,可省去追查一些已被修復的問題。延伸閱讀(傳統上 CU 測試與驗證完整度不如 SP,但近年來 CU 的可靠性已接近 SP
  • GDR,General Distribution Release
    一些重大或安全相關更新的累積更新,若沒有額外資訊測試 CU,GDR 是風險較低的選擇。
  • SP,Service Pack
    可視為特殊標示的 CU,包含安全更新、Bug 修復,有時還會包含新功能。

以下為版本偵測結果範例:

---------------------------------------------------------------------------------------------------------
--//Your current Microsoft SQL Server information:
---------------------------------------------------------------------------------------------------------
Product Version:          11.0.5343.0
Product Name:             SQL Server 2012
Product Level:            SP2 + Security update(GDR)
Product Edition:          Developer Edition (64-bit)
---------------------------------------------------------------------------------------------------------
Note, if you want to know information about CU, please read this KB below.
KB321185, 
---------------------------------------------------------------------------------------------------------
Support Lifecycle stage: Mainstream Support Phase. For additional information refer to 
https://support.microsoft.com/en-us/lifecycle/search?sort=PN&alpha=SQL%20Server&Filter=FilterNO, and Q6, Q18
in the FAQ section of Support Lifecycle page at: https://support.microsoft.com/en-us/lifecycle#gp/lifePolicy
---------------------------------------------------------------------------------------------------------
Full information:
Microsoft SQL Server 2012 - 11.0.5343.0 (X64) 
	May  4 2015 19:11:32 
	Copyright (c) Microsoft Corporation
	Developer Edition (64-bit) on Windows NT 6.3  (Build 9600: ) (Hypervisor)

---------------------------------------------------------------------------------------------------------
--//Recommended updates: 
--### RTM -> QFE or GDR
--### SP  -> QFE or GDR
--### QFE -> QFE
--### GDR -> GDR or QFE
---------------------------------------------------------------------------------------------------------
Install the latest service pack:              SP3, 
Install the latest Cumulative Update (CU) of SP3:  CU7, 
---------------------------------------------------------------------------------------------------------
###### QFE branch updates
---------------------------------------------------------------------------------------------------------
11.0.2376 (SQL Server 2012 RTM QFE) http://support.microsoft.com/en-us/kb/2716441
11.0.3513 (SQL Server 2012 SP1 QFE) https://support.microsoft.com/en-us/kb/3045317
11.0.5613 (SQL Server 2012 SP2 QFE) https://support.microsoft.com/en-us/kb/3045319
---------------------------------------------------------------------------------------------------------
###### GDR branch updates
---------------------------------------------------------------------------------------------------------
11.0.2218 (SQL Server 2012 RTM GDR) https://support.microsoft.com/en-us/kb/2716442
11.0.3153 (SQL Server 2012 SP1 GDR) http://support.microsoft.com/kb/2977326/en-us
11.0.3156 (SQL Server 2012 SP1 GDR) https://support.microsoft.com/en-us/kb/3045318
11.0.5343 (SQL Server 2012 SP2 GDR) https://support.microsoft.com/en-us/kb/3045321
---------------------------------------------------------------------------------------------------------
Note, if you don't want to upgrade to latest service pack right now, we recommend you install the latest
Cumulative Update CU16 of SQL Server 2012 SP2.
Install the latest Cumulative Update (CU) of SP2: CU16, 


---------------------------------------------------------------------------------------------------------
--//You can upgrade to any of the following product(s):
---------------------------------------------------------------------------------------------------------
SQL Server 2014 Developer
SQL Server 2016 Enterprise
SQL Server 2016 Business Intelligence
SQL Server 2016 Standard
SQL Server 2016 Web
SQL Server 2016 Developer


For additional information about supported version and edition upgrades refer to:
https://technet.microsoft.com/en-us/library/ms143393(v=sql.120).aspx

Windows Open JDK 替代方案研究

0
0

前陣子分享過 Oracle 追討 Java 授權費議題的研究心得,原以為誤用同綁安裝的進階商業功能是主要陷阱,但依最近蒐集到的情資,發現對企業用戶而言,「General Purpose Computing」才是天大的坑。依據 Binary Code License(BCL),Java SE 8 只可免費用於「General Purpose Computing」,用於 Embedded Device 或 Other Computing Environment 就得付費。(參考 The current version of Java - Java SE 8 - is free and available for redistribution for general purpose computing. Java SE continues to be available under the Oracle Binary Code License (BCL) free of charge. JRE use for embedded devices and other computing environments may require a license fee from Oracle. )至於在企業內部的個人電腦安裝及執行 Java SE 是否屬於 General Purpose Computing 用途?BCL 主張「 The use of the software in systems and solutions provides dedicated functionality」就不能視為 General Purpose Computing ,除非鬧上法院,是不是 General Purpose Computing 誰說了算?你我的膝蓋都知道答案。

對企業而言,遠離付費爭議的最好方法是離 Oracle Java SE 愈遠愈好,網路上很多人推薦改用 OpenJDK,但 OpenJDK 官網只針對主流 Linux 提供安裝套件,建議 Solaris、Mac OS X、Windows 使用者去找 Oracle。(啊啊啊啊~)

爬文找到一些 Windows 使用 Open JDK 的解決方案:

  1. RedHat 版
    RedHat 從 2016 年 6 月開始提供 Windows 版 Open JDK,使用者可下載 MSI 在 Windows 安裝,其初衷是方便開發者在 Windows 開發與測試 JBoss Middleware 相關程式,下載前需先填寫資料註冊開發者會員,程序繁瑣了點。
  2. 開源社群編譯版本 ojdkbuild
    由開源社群熱心維護:https://github.com/ojdkbuild/ojdkbuild可直接下載安裝,且持續定期更新。
  3. JDK8 Reference Implementation
    官方提供的 JDK8 實作範例:https://jdk8.java.net/java-se-8-ri/,Linux、Mac OS X、Solaris 各平台都有,分 GPL 與 BCL 兩種授權。
  4. Azul System Zulu
    Azul System 這家公司推出基於 Open JDK 開發的開源 Zulu,標榜通過官方 TCK 認證,與 Java SE 完全相容,開放免費下載及使用,Azul 公司則靠技術支援服務營利。

經評估與測試,RedHat 版需註冊才能下載有點麻煩;Reference Implementation 只能算是 PoC 不會持續發展;開源社群版安裝檔下載與安裝都很方便,唯一的小缺點是 MSI 欠缺數位簽章可能被資安單位挑剔;Zulu 通過 TCK 認證且有廠商背書,感覺更可靠,但商業產品多少會隱含授權政策改變的風險。

最後選擇 ojdkbuild 版,下載 MSI 安裝完畢,小試了 HelloWorld,編譯與執行都沒問題:

接著來測試複雜一點的,我刻意挑戰了 Oracle 自己的 Oracle SQL Developer,選擇下載不含 JDK8 的安裝檔。在沒有安裝 Oracle Java SE JDK 的狀況下,程式啟動時會詢問 JDK 位置,將其指向 ojdk-build 的安裝路徑(留意路徑不需包含 bin):

薑薑薑薑~ Oracel SQL Developer 可以使用 ojdkbuild 版 JDK 執行沒問題。

連 Oracle SQL Developer 用 ojdkbuild 版 JDK 都能過關,使用 Open JDK 執行 Java 應該不用太擔心。比較麻煩的是網頁內嵌 Java Applet 的應用情境,Oracle Java SE 提供與 IE、Firefox、Safari 等瀏覽器整合的 Plugin,如要改用 Open JDK 得自己想辦法。IcedTea-Web計劃以自由軟體形式推出允許瀏覽器執行 Java 的套件,但很遺憾沒有現成的 Windows 版實作,相關的研究也很稀少,在 Github 找到一個未成形的 IcedTea-Web for Windows 專案,幫助不大,Zulu 也坦承沒有加入支援的計劃,加上 Java Applet 技術目前已是風中蟾蜍殘燭來日無多(Chrome 很阿莎力地從 42 版就不再支援),預期不會有人再投注心力,這塊看來是無解了。要在 Windows 續用 Java Applet,只能回歸 Oracle Java SE,或許也是一項促使 Java Applet 開發商更快轉向 HTML5 的理由。

一開始順順利利解掉大半問題,最後卻在 Java Applet 挨了一記回馬槍,好有吃鍋貼的感覺~(補聲暗)

崇尚自由開放的 Java 被 Oracle 吹皺一池春水,而 C#/.NET 則逐步走向自由開放,還跨了平台,世界真奇妙。

【茶包射手日記】程式安裝與解除安裝疑難排解員

0
0

來了!來了!從山坡上輕輕地爬下來了。Visual Studio 2017 3/7 RTM囉~

家裡跟公司有好幾台機器要裝,照著小朱的教學文抓好離線安裝包(我選 Enterprise 英文版,全部安裝檔約 20.6 GB),避免逐台重複下載耗時費頻寬又不環保。按照慣例,身為茶包射手體質異於常人,安裝 Visual Studio 一次 OK 成何體統?(案例案例案例)是的,我又踩到水坑了~

本次遇到的問題安裝過程出現 Microsoft.VisualStudio.WebDeploy 安裝失敗,導致 .NET Core、.NET 桌面開發及 ASP.NET 與網頁程式開發裝不起來。

無法安裝套件 'Microsoft.VisualStudio.WebDeploy.Msi,version=15.0.26208.0,chip=x64'。
    搜尋 URL: https://aka.ms/VSSetupErrorReports?q=PackageId=Microsoft.VisualStudio.WebDeploy.Msi;PackageAction=Install;ReturnCode=1316
    受影響的工作負載
        .NET Core 跨平台開發 (Microsoft.VisualStudio.Workload.NetCoreTools,version=15.0.26208.0)
        .NET 桌面開發 (Microsoft.VisualStudio.Workload.ManagedDesktop,version=15.0.26208.0)
        ASP.NET 與網頁程式開發 (Microsoft.VisualStudio.Workload.NetWeb,version=15.0.26208.0)
    受影響的元件
        ASP.NET 與網頁程式開發工具 (Microsoft.VisualStudio.Component.Web,version=15.0.26208.0)
        Web Deploy (Microsoft.VisualStudio.Component.WebDeploy,version=15.0.26208.0)
        Windows Communication Foundation (Microsoft.VisualStudio.Component.Wcf.Tooling,version=15.0.26208.0)
    記錄
        C:\Users\Jeffrey\AppData\Local\Temp\dd_setup_…_Microsoft.VisualStudio.WebDeploy.Msi.log
    詳細資料
        MSI: C:\ProgramData\Microsoft\VisualStudio\Packages\Microsoft.VisualStudio.WebDeploy.Msi,version=15.0.26208.0,chip=x64\webdeploy_x64.msi,屬性:  REBOOT=ReallySuppress
        傳回代碼: 1316
        傳回代碼詳細資料: 指定的帳戶已存在。

爬文推測為 Web Deploy 套件之安裝資訊毁損,導致移除或升級失敗。試著手動解除安裝,看到一模一樣的「指定的帳戶已存在訊息」。

針對無法安裝或無法解除安裝的疑難雜症,微軟有個自動修復工具,能修復以下問題:

  • 64 位元作業系統的登錄機碼損毀
  • 控制更新資料的損毀登錄機碼
  • 無法安裝新程式問題
  • 無法解除安裝或更新現有程式問題
  • 無法由 [控制台]中透過 [新增或移除程式] (或 [程式和功能]) 解除安裝程式的問題

從沒用過,抱著姑且一試的心情試跑(命運之神會這麼輕易放過我嗎?),程式詢問要解除安裝的程式,Microsoft Web Deploy 3.6 不在其中,依提示選取「未列出」:

第一關來了,請填入產品代碼 GUID… 嗯,還好我對 GUID 也是略懂略懂,就算不知產品代碼,還是可以用暴力破解,估計試過 103 百萬兆次就會有 10 億分之一的成功機會:

別鬧了!身為 Windows 老鳥,很快在 Registry 搜尋關鍵字找出答案。

答案正確,程式成功找到「Microsoft Web Deploy 3.6」,詢問要解除安裝還是嘗試其他修正,選擇解除安裝。

之後經過「毁損的修補登錄機碼」(這啥?阿鬼,你還是說英文吧!)、「查看修補程式相關問題的登錄」、「嘗試使用下列項目解決問題:Microsoft Web Deploy 3.6」等過程,大功告成!

Microsoft Web Deploy 3.6 從解除安裝清單消失,重試一次,Visual Studio 2017 安裝完成,萬歲!

找到工具這麼快就把問題解了,還真不習慣,哈!(謎:是有沒有這麼賤骨頭啦?)

Windows 停用 TLS 1.0 之配套作業整理

0
0

開始之前,說說 TLS。

大家朗朗上口的 SSL(Security Socket Layer),最早源於 1995 年發表的 SSL 2.0(1.0 很雷,所以從沒公開過),隨後在 1996 推出 3.0 版,IETF 於 1999 年將 SSL 標準化,因版權考量改稱為 TLS(Transport Layer Security)。就技術而言, TLS 1.0 與 SSL 3.0 很相近,而 TLS 1.0 也支援降級改用 SSL 3.0。之後 IETF 分別在 2006、2008 年再推出安全強度更高的 TLS 1.1 與 TLS 1.2。

2014 年,Google 發現 SSL 3.0 存在致命安全漏洞,而攻擊者可藉由向 TLS 發送假的錯誤提示降級至 SSL 3.0,再利用 SSL 3.0 的漏洞竊取資訊,各家瀏覽器紛紛決定禁用 SSL 3.0。因此大家常說的 SSL,其實早已經被 TLS 取代。參考來源

TLS 1.0 因 CBC 加密模式設計不良,可能遭受 BEAST 攻擊導致加密內容被解密,便落入與 SHA1 相同的命運-被業界宣判限期下架,於是,「停用 SSL 3.0 與 TLS 1.0」成為資安檢核項目之一,建議系統管理人員早日關閉。

要在 Windows 停用 TLS 1.0、啟用 TLS 1.1、TLS 1.2,只需修改 Registry即可完成。且慢,先別急著動手,刺激的在後面… 以下是這陣子雞飛狗跳摸石頭過河後的心得整理,短短幾行卻血淚交織,價值不斐:

  1. 停用 TLS 1.0 後遠端桌面連不上
    「用遠端桌面登入主機,設好 Registry 重開機後就再也連不進去了」這種劇情很驚險刺激吧?
    Windows 7 及 Windows 2008 R2 需先安裝更新,遠端桌面程式需更新到新版才支援 TLS 1.1/1.2 連線。
  2. 停用 TLS 1.0 後 SQL Server 起不來
    「 資料庫主機停用 TLS 1.0 並重開機,SQL Server 就起不來了」這種橋段也扣人心弦吧?
    在事件檢視器可看到類似錯誤: 
    A fatal error occurred while creating an SSL client credential. The internal error state is 10013.
    MVP Aaron Bertrand 有篇 Blog詳細整理各版本 SQL Server 要支援 TLS 1.2 需要的最小版號,至於 SQL 詳細版本資訊的查詢方式,可參考前幾天的文章
  3. SQL Server 停用 TLS 1.0 後,.NET 程式無法連上資料庫
    SQL 更新並停用 TLS 1.0 後,原本使用 SqlConnection 連線 SQL 的 .NET 程式可能出現以下錯誤訊息:
    使用 Integrated Security=SSPI 以 AD 登入 - A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: Shared Memory Provider, error: 0 - No process is on the other end of the pipe.)
    使用 SQL 帳號登入 - A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
    是的,ADO.NET 也得更新,請參考微軟這篇 KB,依據使用的 .NET 版本安裝對應的更新。除了 .NET 4.6,.NET 2.0/3.5/4.0 到 4.5.2 都需更新才能以 TLS 1.2 連上 SQL。
  4. 連線 TLS 1.2 HTTPS
    .NET 客戶端使用 WebClient、WCF 以 HTTPS 連線遠端主機,也會涉及 TLS 1.0/1.1/1.2 版本議題,不同版本 .NET 的處理方式不同:
    .NET 4.6內建支援且預設使用 TLS 1.2
    .NET 4.5內建支援,但需透過 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 設為預設協定
    .NET 4本身不支援,但安裝 .NET 4.5 後即可使用 TLS 1.2,指定 TLS 1.2 的寫法為 ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
    (註:若不想修改 .NET 4/4.5 程式,也可透過 Registry修改預設安全協定)
    .NET 3.5需安裝 Hotfix 才能支援
        KB3154518 – Reliability Rollup HR-1605 – NDP 2.0 SP2 – Win7 SP1/Win 2008 R2 SP1
        KB3154519 – Reliability Rollup HR-1605 – NDP 2.0 SP2 – Win8 RTM/Win 2012 RTM
        KB3154520 – Reliability Rollup HR-1605 – NDP 2.0 SP2 – Win8.1RTM/Win 2012 R2 RTM
        KB3156421 -1605 HotFix Rollup through Windows Update for Windows 10.

事後來看遇上這些狀況合情合理,當初處理過程可不是這麼一回事:

遠端登入改完 Registry 重開機,之後遠端桌面連不進去、連進去發現 SQL Server 起不來、把 SQL 救起來後發現連 SQL 的 .NET 程式壞光光… 不知所以然胡搞瞎試搞完一回合,學新知還兼練心臟,好激刺呀~

以上是目前我蒐集到停用 TLS 1.0 所需的配套更新,未來如有發現再陸續補充更新。

客製靜態檔案 HTTP 404 訊息

0
0

同事報案,某組 Windows 2012R2 Web Farm 均已設定 web.config <customErrors mode="On" /> HTTP 404 網頁理應如下:

但 Web Farm 其中一台卻會顯示詳細錯誤,導致實體路徑資訊外洩:

最後同事找出原因,IIS Error Pages 設定有個 Edit Feature Settings,問題主機被設成「Detail Errors」:

心中對這組設定與 customErrors 的關係滿心狐疑,爬文後才驚覺自己寫過文章:ASP.NET 相關程式錯誤由 <system.web><customErrors> 控制,靜態檔案(html、gif、png、jpg、js、css)則由 <system.webServer> <httpErrors> 決定,不過一年多前的事竟忘得一乾二淨,特再撰文一篇加強印象,確保此生不忘,阿彌陀佛~

Viewing all 2304 articles
Browse latest View live




Latest Images